19.6 Type=simple vs forking vs notify vs oneshot
Right, let’s settle this. You’re about to configure the Type= directive, and this is where most people’s service units go from “theoretically correct” to “actually works.” The Type tells systemd how to manage your service’s main process, and getting it wrong means systemd will either lose track of your process or sit around waiting for a signal that’s never coming. It’s the difference between a well-trained dog and one that just ran into the woods chasing a squirrel.
We have four main types to pick from. Let’s break them down, from the most common to the most… particular.
Type=simple: The Default (For Better or Worse)
This is the default for a reason, and that reason is often “laziness.” Type=simple is for services where the process you start is the main daemon process that does all the work. The key thing to know: systemd assumes this process is ready to rock the moment it forks it. It does not wait.
[Unit]
Description=A Simple Web Server
[Service]
Type=simple
ExecStart=/usr/bin/python3 -m http.server 8080
Seems straightforward, right? Here’s the pitfall: if that Python module has a brief startup time where it might crash (e.g., because the port is already taken), systemd won’t know. It has already declared the service “started” and moved on. By the time you run systemctl status, it might already be dead. simple is fine for quick-and-dirty stuff, but for anything serious, you often want a bit more control. It’s like trusting a stranger with your house keys because they seem nice.
Type=forking: The Classic Daemon’s Best Friend
This is for the old-school, well-behaved daemons that… well, fork. The pattern is: you start a process, it does some setup, forks a child process to do the real work, and then the parent process exits. The exit code of the parent tells you if it succeeded. This was the standard way to daemonize before systemd came along.
systemd needs to know which process is the actual daemon, which is why you’ll almost always pair this with PIDFile=.
[Unit]
Description=Old School Daemon
[Service]
Type=forking
ExecStart=/usr/sbin/my_classic_daemon -d
PIDFile=/var/run/my_classic_daemon.pid
# Usually necessary for forking services
Restart=always
Here’s the absurd part: the entire reason this type exists is to handle the fact that the original process exits. systemd is brilliant at tracking processes, but it has to have a whole special mode just to handle this one weird trick from the 1970s. The pitfall? If your daemon doesn’t actually fork or doesn’t reliably write its PID file, systemd will be left waiting forever for a child that doesn’t exist, eventually timing out. It’s a fragile, holdover mechanism. We use it because we have to, not because we want to.
Type=notify: The Modern, Sane Choice
This is what you should use for any new service you write that can be taught new tricks. With Type=notify, the service process is expected to send a specific message to systemd (via the sd_notify() system call or a library equivalent) when it is truly initialized and ready to receive requests.
This is a vast improvement. systemd doesn’t just assume it’s ready; it knows it’s ready. It also allows for better synchronization; you can use After= and Requires= to order services, and systemd will actually wait for the “ready” signal before starting the next one.
[Unit]
Description=A Modern Notifying Service
[Service]
Type=notify
ExecStart=/usr/bin/my_go_service
# WatchdogSec=... # You can even add a watchdog feature with this type
# Example in Go code to send the notify signal:
package main
import (
"github.com/coreos/go-systemd/daemon"
"time"
)
func main() {
// ... do your initialization work ...
time.Sleep(2 * time.Second) // Simulating startup work
// Tell systemd we're ready!
daemon.SdNotify(false, daemon.SdNotifyReady)
// ... now run your main event loop ...
}
The pitfall? Your software has to actually implement this. If it doesn’t send the “READY=1” signal, systemd will again wait until it hits a timeout and assume your service failed to start. But when it works, it’s beautiful. It’s your service telling systemd, “I’m open for business,” instead of systemd guessing.
Type=oneshot: For Jobs That Do One Thing and Exit
This one’s a bit of an outlier. You use oneshot for things that aren’t long-running daemons. They are processes that run, do their job, and exit. Think of it like a script run by systemd. This is the type you’d use for a task that prepares the filesystem before a service starts.
The critical thing here is the RemainAfterExit= flag. Without it, once the ExecStart process exits, the service is considered “inactive” again. If you set RemainAfterExit=yes, the service will be considered “active (exited)” after a successful run, which is useful for state-tracking.
[Unit]
Description=Create a necessary directory on boot
[Service]
Type=oneshot
ExecStart=/bin/mkdir -p /var/lib/myapp/data
ExecStart=/bin/chown myapp:myapp /var/lib/myapp/data
RemainAfterExit=yes
# This runs before our main app service
[Install]
WantedBy=multi-user.target
The pitfall is trying to use this for a daemon. If you point a oneshot at a process that doesn’t exit, systemd will be permanently stuck in the “activating” state, waiting for it to finish. It’s the right tool for a specific job, not a general-purpose tool.