Systemd Type= simple, forking, notify: Wetin be the diff?
Your service dey show active even though the daemon don die? Learn how to choose the right Type for simple, forking, and notify to track your main PID and stop ghost processes.
Why systemd dey report say unit dey active even though the process don die
One systemd service unit dey stay active as long as the process wey systemd call the main process still dey alive, and na the [Service] section for inside the Type= file dey decide which process be that. If you pick the wrong value, systemd go just dey watch one shell wrapper or parent process wey dey finish quick, even if the main daemon wey you care about don die for inside the same unit. The unit dey tell you the truth about the process wey dem tell am say e suppose watch.
If you change the restart policy, e no go help for this matter. Restart= only dey work when the main process exit, so Restart=always no go ever fire as long as the main PID (process identifier) still belong to something wey dey run. You must fix Type= first. Wetin systemd go do after the main process truly exit na separate decision, wey we don explain for inside the guide to Restart= and RestartSec=.
Wetin Type= actually dey decide
Every Type= value dey answer two questions at once. When systemd fit consider say this unit don start, and which process be the main one.
The first answer dey control ordering. Any unit wey mention your own for After= go wait until systemd call your own say e don start. Any Type= wey report "started" too early go make dependent units run before your service fit answer dem.
The second answer dey control supervision. systemd dey put every process wey unit spawn inside one cgroup (control group), na kernel feature wey dey group processes so dem fit limit and kill dem together. The cgroup na how systemctl stop dey clean up: KillMode= defaults to control-group, so if you stop unit, e go send signal to every process inside am. The main PID dey narrower. E be the single process wey if e exit, the unit go end, and the exit status go become the unit result. To read the cgroup as if e be the main PID na where the confusion dey start.
Type=simple dey show say service start before the binary run
Type=simple na im be the default when you set ExecStart= and you no put Type= or BusName=. systemd go create the process, take the unit as say e don start sharp-sharp, and treat that process as the main PID. Units wey dey follow go start immediately, even before the service binary don run finish.
That last point explain why people dey surprise sometimes. If you make mistake for the ExecStart= path, the start job go still show say e succeed, but the failure go show small time later when the execution fail. systemd go record that case with exit code 203, wey the table call EXEC, and e mean say the service binary no fit run. So, if systemctl start return without error, e no mean say your binary dey there.
Use simple for program wey dey stay for foreground and no dey move itself go background. This one cover most modern daemons and almost anything wey you write by yourself.
Type=exec dey wait make the program start finish
Type=exec na simple wey get one extra step. systemd go consider say the unit don start only after the fork and the execution of the binary don succeed. If the binary no dey or if User= no fit resolve, the start job go fail sharp-sharp, instead of make e talk say e succeed then fail silently small time later.
Type=exec show for systemd 240, so every server distribution wey dey now get am. Ubuntu 24.04 get systemd 255 and Debian 13 get systemd 257, as of August 2026. Check your own with systemctl --version.
The price be say one extra synchronisation step go dey when e start. The gain be say you go get honest exit status from systemctl start. For program wey dey run for foreground, make you prefer exec pass simple.
Type=forking, and how the main PID dey loss
Type=forking dey tell systemd say the process wey dey ExecStart= go fork one child process then exit on purpose. systemd go wait make that first process exit before e go mark the unit as started. The child wey remain na im be the daemon. That style start from SysV time, when nothing dey supervise daemon once the init script return, and PID file na im be the only record of wetin dey run. That limitation na one of the main reason why systemd replace init scripts.
The wahala na identity. The process wey systemd launch don comot, so systemd must find out which survivor be the main one. Set PIDFile= to the file wey the daemon dey write, usually one path under /run, and systemd go read the PID from there. systemd go also check say the PID for that file belong to one process wey dey under this service, so if the file get old PID wey no concern the service, systemd go reject am instead of to trust am.
Without PIDFile=, GuessMainPID= go apply, and e dey default to yes. This guess only dey reliable when the service settle into one single process. The manual state the limit clearly: if the daemon get more than one process, the guess fit wrong, and failure detection go stop to work. One unit fit even end up with main PID of 0, wey mean say systemd no get anything at all to supervise.
Most daemons wey dey fork also get one switch wey fit make dem stay for foreground. Use that switch with Type=exec and delete the PIDFile= line. Fewer moving parts mean fewer ways to lose the PID.
Type=oneshot sake of work wey dey finish
Type=oneshot dey expect say the process go run finish come exit. systemd go call the unit "started" only after e don exit, and this one make oneshot the correct shape for anything wey another unit must wait for. Na this one self be the default wey systemd dey use if unit no specify Type= or ExecStart=.
Two behaviours dey special for oneshot. Na only this type dey accept more than one ExecStart= line, and those lines go run one after another. The start timeout self dey disabled by default, so if oneshot hang, e go wait forever unless you set TimeoutStartSec= by yourself.
After the process exit, the unit go return to inactive. RemainAfterExit=yes go keep am active even if no process dey run again. This one na the deliberate version of the wahala wey we talk about for top of this page, and e correct when the unit job na to leave state behind instead of to keep something dey run: like to load firewall ruleset, or to bring up container stack. Na this pattern dey behind Docker Compose stack wey dey come back after reboot, where the unit run the compose command, exit, and stay active because the containers wey e start still dey run. oneshot unit self na wetin schedule dey trigger, and na the other half of how to run job for systemd timer instead of cron.
Type=notify dey make service tell systemd say e don ready
Type=notify dey move the decision make the service decide by e-self. systemd go hold the start job open until the process send READY=1 pass one Unix socket wey the path dey inside the NOTIFY_SOCKET environment variable. The C interface na sd_notify(3), and plenty servers don support am already.
Dis one na the correct answer to the question "e don start?". simple and exec dey report say service don start even before the service read e configuration or open the listening socket, so one dependent unit fit start too early and fail e first connection. notify dey report say service don start for the exact moment wey the service e-self talk say e don ready.
systemd dey accept that message from the main process only, and na wetin NotifyAccess=main mean be that, and Type=notify dey imply am. If the message come from a child or helper process, set NotifyAccess=all. One shell script fit call systemd-notify --ready, but that one dey run as a separate process wey no dey stay long, so e need NotifyAccess=all and systemd fit no fit know who send the message if the sender don already close. Service wey fit talk the protocol e-self dey more reliable.
Two settings wey relate to this one dey important to know. Type=notify-reload, wey dey available since systemd 253, dey extend the same handshake to reloads, so systemctl reload go return only when the service report say the reload don finish instead of returning immediately after the signal go. WatchdogSec= dey ask notifying service make e send keep-alive message for specific interval, and systemd go treat any missed deadline as failure.
Type=dbus and Type=idle
Type=dbus dey wait until the service take name for D-Bus, dat message bus wey system and desktop services dey use talk to each other. E need BusName=, and e go become the default as soon as you set BusName=. Use am only for service wey truly register bus name.
Type=idle dey behave like simple, but e dey delay the program until all queued jobs don finish, with five seconds limit. E dey so dat console output for boot no go mix with status messages. E no be tool for ordering, and e no suppose dey for normal service.
Why wrapper script dey make systemd track wrong PID
Dis na di pattern wey dey cause di wahala.
[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 di shell as di main PID. Di shell go still dey run while exporter dey work for foreground. If server die, di shell no go know, so di main PID go still dey active, di unit go still show as active, and Restart= no go get anytin to do. Both processes go still dey inside di unit cgroup, so systemctl stop go still fit clean up well. Na supervision break, no be cleanup.
Di way to fix am depend on how many long-running processes di unit get.
If na only one, replace di shell wit am.
#!/bin/bash
export APP_ENV=production
exec /opt/app/bin/server --config /etc/app.yamlexec go replace di shell wit di program wey you name and e go keep di same PID, so di PID wey systemd record go now belong to di daemon. Better still, comot di wrapper. Environment= and EnvironmentFile= fit carry di variables, and ExecStartPre= fit handle di setup step, so systemd fit launch di daemon directly and know di PID from di start.
If dem be two, no single PID fit represent di unit. Split dem into two units and arrange dem wit After= and Wants=. One unit per process na di arrangement wey systemd sabi supervise well, and na only dat way each process go fit get im own restart behaviour.
Wetin ExitType=cgroup dey change
ExitType= dem add am for systemd 250. The default na main: dem go consider say the unit don stop once the main process exit. With ExitType=cgroup, dem go consider say the unit still dey run as long as any process wey dey inside the cgroup still dey alive.
[Service]
Type=simple
ExitType=cgroup
ExecStart=/opt/app/launcherThis one solve one specific problem. If you get launcher wey start the real work then exit, under ExitType=main, systemd go think say the unit don stop and e go kill the remaining processes. With ExitType=cgroup, the unit go follow the whole group instead.
Make you clear about wetin e no fit solve. ExitType=cgroup go keep unit active as long as at least one process dey alive, so if unit dey hold two daemons, e go stay active even after one of dem die. E dey fix the launcher case. E no dey turn one unit into supervisor for plenty independent processes. ExitType= no fit join with Type=oneshot too.
The cgroup na where resource accounting dey land, so limits like MemoryMax= and CPUQuota= go apply to every process wey the unit spawn, no matter wetin Type= talk about the main PID. That side of the matter dey inside capping a service's memory and CPU with systemd.
How to find the process wey systemd dey watch
Follow this steps for the unit wey you dey debug, one by one. Read wetin systemd load, check 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 go print the unit file join with every drop-in wey apply to am, so you go fit read wetin systemd load instead of the file wey you think say you edit. systemctl show go print the effective values, including the defaults wey you no 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 go list every process wey dey inside the unit cgroup. The ps line dey describe the single process wey systemd dey supervise. Read the two together. A MainPID of 0 mean say systemd no get any process to watch. A MainPID wey point to a shell while the cgroup still get your daemon mean say the wrapper case wey we talk about don happen. A cgroup wey get more processes pass wetin you expect mean say a launcher or a forking daemon dey involved.
systemctl status app.service
journalctl -u app.service -bsystemctl status go print the state line and the cgroup tree together, so e dey often answer both questions at once. journalctl -u wey you limit to this boot with -b go show the start and stop events wey systemd record for the unit, join with the exit codes wey e see. If the daemon write to e 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.
systemd-analyze verify /etc/systemd/system/app.service
sudo systemctl daemon-reload
sudo systemctl restart app.servicesystemd-analyze verify go parse the file and report settings wey e no fit accept. daemon-reload go make systemd re-read unit files from disk. A changed Type= no go apply to a unit wey already dey run, so the restart na must, e no be option.
Then test the change. Take the PID of the process wey you really care about from systemd-cgls and kill am. Run systemctl is-active app.service immediately after. If Type= correct, the unit go comot from the active state. If e still dey active, e mean say systemd still dey watch something else.
Which systemd service Type= you suppose use
- One program wey dey stay for foreground:
Type=exec. - One program wey support readiness notification:
Type=notify, andnotify-reloadif e still confirm reloads. - One daemon wey insist say e must go background:
Type=forkingwithPIDFile=, or use the foreground switch withType=exec. - One script wey do work finish come exit:
Type=oneshot, plusRemainAfterExit=yesif the main reason na to leave state behind. - One launcher wey exit while e children still dey run:
Type=simplewithExitType=cgroup.
If you no sure which one one third-party daemon need, first read the unit file wey come with am. To run systemctl cat on one unit wey the distribution provide go show you the Type= wey the upstream choose, and plenty people don test that choice pass your own.
FAQ
Why my systemd unit still dey active even when the process don die?
Because the process wey systemd dey track as the main one still dey alive. systemd dey watch only one PID per service, wey dem choose base on Type=, e no dey watch every process inside the unit cgroup. Na wrapper script wey you start with Type=simple dey cause this one mostly: the shell na the main PID, so the unit go stay active even if the daemon wey the shell launch for background don exit. Run systemctl show -p MainPID app.service, then list the unit cgroup with systemd-cgls --unit=app.service, and compare the two.
Wetin be the difference between Type=simple and Type=exec?
Type=simple go consider say the unit don start as soon as systemd don create the process, even before the binary don execute, so if the path for ExecStart= wrong, the start job go still show say e succeed before e later fail. Type=exec go wait until the execution succeed, so if e fail, the start job go report the error direct. Both of dem dey treat the same process as the main PID. Type=exec need systemd 240 or newer.
I still need PIDFile= if I dey use Type=forking?
Yes, anytime the daemon dey write one. If you no put am, systemd go fall back to GuessMainPID=, wey be just guess and e only dey work well for service wey just get one process. If the guess wrong or e no possible, the system no go fit detect failure or restart the unit automatically. Point PIDFile= to the exact path wey the daemon dey write, usually under /run.
When I suppose use RemainAfterExit=yes?
When the work of the unit na to change system state, no be to keep process dey run. A Type=oneshot unit wey dey load firewall rules or start container stack go exit as soon as e finish the work, and if you no use RemainAfterExit=yes, the unit go show as inactive. This one go make systemctl stop no get anything to stop and no way to run ExecStop= cleanup. If you use am, the unit go stay active even if no process dey run, and na so e suppose be.
If I change Type=, I need to do daemon-reload?
Yes, and you must restart the unit too. systemctl daemon-reload go make systemd read the unit files for disk again, but the instance wey dey run go still keep the Type= wey e start with. Run sudo systemctl daemon-reload and then sudo systemctl restart app.service before you test, if you no do am, you go still dey watch the old supervision behaviour.