systemd: Explanation & Insights
The init system and service manager used by most modern Linux distributions to boot the machine and start, stop, and supervise services.
What It Is
systemd is the first program Linux runs after the kernel finishes loading, and the program that stays running until the machine shuts down. It is the init system — process number 1, the ancestor of every other process on the box — and at the same time the service manager: the thing that starts your web server, restarts it when it crashes, and tells you why it won't start at 2 a.m.
That second job is what makes systemd more than a classic init. An older init's whole life was: run a pile of shell scripts at boot, then sit quietly as PID 1 and do almost nothing until shutdown. systemd never stops working. It holds a live map of everything that should be running, watches each piece, and acts on what it sees — start this, that one died so bring it back, this can't start until the network is up.
The single idea underneath all of it is the unit: one uniform description of a thing systemd manages. A service is a unit. A mount point is a unit. A timer, a network socket, a target — all units, all described in the same small .ini-style text file with the same vocabulary. Learn to read one unit file and you can read all of them. That uniformity is the whole trick, and most of this page is about what it buys you.
You drive the entire thing through one command, systemctl. Want to know if a service is healthy? systemctl status. Start it, stop it, have it come back on boot? systemctl start, stop, enable. One verb, one noun, and it works the same for the database, the firewall, and the disk mount. After years of every daemon having its own /etc/init.d/thing restart incantation, that consistency lands like a cool drink.
systemd was written in 2010 by Lennart Poettering and Kay Sievers, then both at Red Hat, and within five years it had become the default init on essentially every major Linux distribution. It is also the most argued-about piece of software in the Linux world — we'll get to why, because the argument is genuinely interesting and you should understand both sides.
Why It Matters
If you run a Linux server, systemd is not optional and it is not in the background — it is the thing you reach for constantly. Every long-running program on a server is a daemon (a process that detaches from your terminal and runs forever in the background — the name is an old MIT joke about a helpful background spirit, not the horned kind). Your job as the person running the box is to make those daemons start at boot, come back after a crash, and tell you what they're doing. That is precisely systemd's job too, which means you and systemd are doing the same work — the only question is whether you do it by hand or let it.
Before systemd, the answer was "by hand, in shell." You wrote (or inherited) a script in /etc/init.d — fifty lines of Bash that knew how to launch your daemon, write down its process ID in a file, and later read that file back to stop it. Every service shipped its own script, each subtly different, each able to lie to you: the script could report "started" while the daemon had already fallen over a second later, because all it really checked was whether the launch command returned. The pidfile it wrote could go stale, pointing at a process number the kernel had since handed to something else entirely.
systemd replaces every one of those scripts with a dozen-line declaration of intent — what the service is, what it needs, what to do when it dies — and then takes responsibility for making reality match. That shift, from a script that does the starting to a description of what should be true, is the thing to hold onto. You stop writing procedures and start stating facts; systemd is the engine that makes the facts true and keeps them true. When something is wrong, you ask it, and because it has been watching the whole time, it actually knows.
The Unit: One Abstraction to Rule Them All
A unit is a thing systemd manages, described in a plain text file. The most common kind is the service — a .service unit — but the genius is how many other things wear the same clothes:
.service— a daemon to run and keep running (your web server, your database)..socket— a network or file socket systemd opens on the service's behalf. More on this below; it's the cleverest piece..target— a named grouping of other units, used to reach a state ("everything needed for a normal multi-user server"). Targets replaced the old runlevels — see below..timer— a schedule that activates another unit. This is systemd's answer tocron: instead of a line in a crontab, a timer unit that triggers a service unit..mount/.automount— a filesystem mount, expressed as a unit instead of a line in/etc/fstab(thoughfstabentries are quietly turned into mount units for you)..device— a piece of hardware the kernel told systemd about, so other units can depend on it appearing..slice/.scope— groupings for resource control, which we'll meet in the cgroups section.
Here is a real, minimal service unit — this is the entire thing, not an excerpt:
[Unit]
Description=My Web App
After=network-online.target
Wants=network-online.target
[Service]
ExecStart=/usr/local/bin/mywebapp --port 8080
Restart=on-failure
[Install]
WantedBy=multi-user.target
Read it top to bottom and you've read the language. [Unit] says what it is and when it may start — After=network-online.target means don't launch me until the network is actually up, and Wants= pulls that network target in as a soft dependency (if it fails, I still try). [Service] is the heart: ExecStart= is the command, and Restart=on-failure is the line that quietly replaces a whole genre of "is my service still alive?" monitoring scripts people used to run from cron. [Install] says where this unit hangs when you enable it — WantedBy=multi-user.target means "when the system reaches normal multi-user state, I should be running."
That's it. The same three-section shape, with different keys, describes a timer, a mount, a socket. One vocabulary for the whole machine.
Note
Units ship in two places and it matters which. Packages drop their units in
/usr/lib/systemd/system/(or/lib/...) — don't edit those; an update overwrites them. Your changes go in/etc/systemd/system/, which wins over the package copy. To tweak one setting without copying the whole file, runsystemctl edit <unit>and write a small drop-in override. After any change by hand,systemctl daemon-reloadso systemd re-reads the files.
How It Works: The Dependency Graph and Parallel Boot
Here is the one thing every other systemd tutorial leaves muddy, so let's nail it down.
An old SysV-init boot was a single-file line of dominoes. Scripts ran one after another in an order fixed by the numbers in their filenames — S10network, S20syslog, S30mysql — and each had to finish before the next began. If S20syslog spent ten seconds waiting on something, every script after it simply waited, even the ones that had nothing to do with logging. A modern server with dozens of services booted as slowly as the sum of its slowest parts, mostly sitting idle.
systemd threw out the line of dominoes and built a dependency graph instead. You never tell it an order. You tell each unit what it needs (Requires=, Wants=) and what it must come after (After=, Before=), and systemd computes the rest: anything whose dependencies are already satisfied can start right now, in parallel, all at once. On a typical server that means the database, the mail system, and the SSH daemon fire up simultaneously, because none of them needs the others. The boot takes as long as the longest chain of real dependencies, not the sum of everything.
But parallel boot has a chicken-and-egg problem, and the way systemd solves it is the cleverest idea in the whole project. Suppose your web app needs the database. If they start at the same instant, the app might reach for a database that isn't listening yet, and fall over. The old fix was to order them — start the database first, wait, then the app — which throws away the parallelism you just bought.
Socket activation sidesteps it. systemd itself opens the listening socket — the port — before starting any daemon at all. The port exists, held open by PID 1, from the very first moment. Now everything can start at once: if the web app connects to the database's port before the database is ready, the connection doesn't fail — it just waits in the socket's queue, held by the kernel, until the database finishes starting and picks it up. The ordering problem dissolves. Nobody has to wait for anybody; they only wait for the bytes they actually asked for.
Backstory
If that sounds familiar, it should: it's a 1980s idea wearing new clothes. The old Unix
inetd"super-server", and later Apple'slaunchd, both did exactly this — open the socket centrally, then hand it to the real daemon on first connection. systemd's leap was to realise the same trick could parallelise the entire boot, not just save memory on idle services. Poettering laid the whole argument out in a 2010 essay bluntly titled "Rethinking PID 1." The good ideas in computing have a way of being thirty years old and still arriving early.
Targets: What Replaced Runlevels
If you've met older Unix you've heard of runlevels — a single number, 0 through 6, that described what mode the machine was in. Runlevel 3 was "multi-user, no graphics" (a normal server); 5 added the graphical login; 0 was off, 6 was reboot. One number, one state, and the machine was in exactly one at a time.
systemd replaces the number with a target — a unit that names a state worth reaching by depending on all the units that state requires. The rough translation:
| Old runlevel | systemd target | What it means |
|---|---|---|
| 0 | poweroff.target |
Halt the machine |
| 1 / S | rescue.target |
Single-user maintenance mode |
| 3 | multi-user.target |
Full server, no graphics |
| 5 | graphical.target |
Multi-user plus a desktop |
| 6 | reboot.target |
Restart |
The difference that matters: runlevels were a flat list, but targets form a graph. graphical.target doesn't repeat everything in multi-user.target — it simply depends on it and adds the display manager on top. The state your server boots into is default.target, which is just a symlink pointing at whichever real target you chose (almost always multi-user.target on a headless box — there's no monitor, so don't pay for a desktop you'll never see). Switch states live with systemctl isolate rescue.target; set the boot default with systemctl set-default multi-user.target.
journald: Where the Logs Went
Because systemd starts every service, it also captures everything every service prints. That capture is journald, the logging half of the project, and it surprises people: the logs are no longer plain text files you can cat. They're stored in a structured binary format, and you read them with journalctl.
This trades one thing for another, and it's worth knowing the trade. You lose the ability to grep a log file directly — the bytes on disk aren't text. You gain structure: every log line is automatically tagged with which unit emitted it, the exact timestamp, the priority, the process ID. So journalctl -u nginx shows you only the web server's lines, across every restart, with no log file to find or rotate. journalctl -p err -b shows only the errors since this boot. The query power is real, and it's why the binary format exists — though plenty of admins still run a traditional text logger like rsyslog alongside it out of habit, and that's fine.
Pro Tip
When a service won't start, the fastest path to the answer is two commands.
systemctl status <unit>shows the last handful of log lines inline — often enough on its own. If it isn't,journalctl -u <unit> -eopens the full history jumped to the end. Nine times out of ten the daemon told you exactly what was wrong; it's just that under the old system that message vanished into a logfile nobody watched.
cgroups: How systemd Actually Tracks a Service
This is the deepest and most underappreciated part, and it fixes a problem that quietly defeated every init system before it.
How do you know which processes belong to a service? The old answer was the pidfile: when the daemon started, it wrote its process ID into a file, and to stop the service you read that number back and killed it. The trouble is that a daemon is free to spawn children, and a child can spawn grandchildren, and a process can deliberately double-fork — fork twice and let the middle parent exit — to detach itself completely from whoever launched it. After that, the pidfile points at one process while three others run on, orphaned and untracked, and stopping the service leaves debris running. A worse version: the kernel recycles process numbers, so a stale pidfile can name a PID that now belongs to something else entirely, and your "stop" command kills an innocent.
systemd doesn't trust process IDs at all. It puts every service into its own cgroup — a kernel-enforced box that labels every process inside it, and crucially, every process those processes ever spawn inherits the same label automatically. The kernel does the bookkeeping, so there is no escaping it.
Note
A daemon that double-forks to slip its parent isn't being clever; it's a toddler who thinks that because it can't see you, you can't see it. The cgroup is the parent who can still see it — the child can fork all it likes, but it's forking inside the box, and the box came from the kernel.
This is what makes systemctl stop reliable where the old scripts were not: systemd asks the kernel to signal the whole cgroup, so a service and every descendant it ever produced go down together, with nothing left behind. And because cgroups also meter resources, the same mechanism lets you cap a service: add MemoryMax=512M or CPUQuota=50% to its unit and the kernel enforces it. The thing that tracks your service and the thing that limits it turn out to be the same thing.
Your First Look
Four commands will tell you almost everything about a systemd machine. Here's what each shows on a real server.
Asking after a single service — the command you'll run most (on Debian-family systems the unit is ssh; on others sshd — systemctl accepts either):
systemctl status ssh
● ssh.service - OpenBSD Secure Shell server
Loaded: loaded (/usr/lib/systemd/system/ssh.service; enabled; preset: enabled)
Active: active (running) since Fri 2026-04-10 06:37:05 UTC; 2 months ago
Main PID: 3101733 (sshd)
Tasks: 1 (limit: 13989)
Memory: 5.1M (peak: 164.7M)
CPU: 2h 39min 34s
CGroup: /system.slice/ssh.service
└─3101733 "sshd: /usr/sbin/sshd -D [listener]"
Read it from the top: the Loaded: line confirms systemd found the unit file and whether it's enabled (starts on boot); the Active: line is the truth of the moment — active (running) and how long it's been up (here, two months). The CGroup: block at the bottom is the cgroup from the section above, listing every process the service owns. Run it as root and a tail of recent journald lines appears beneath, for free.
Everything currently running, at a glance:
systemctl list-units --type=service --state=running
chrony.service loaded active running chrony, an NTP client/server
cron.service loaded active running Regular background program processing daemon
dbus.service loaded active running D-Bus System Message Bus
docker.service loaded active running Docker Application Container Engine
rsyslog.service loaded active running System Logging Service
ssh.service loaded active running OpenBSD Secure Shell server
systemd-journald.service loaded active running Journal Service
systemd-logind.service loaded active running User Login Management
systemd-networkd.service loaded active running Network Configuration
systemd-resolved.service loaded active running Network Name Resolution
systemd-udevd.service loaded active running Rule-based Manager for Device Events
unattended-upgrades.service loaded active running Unattended Upgrades Shutdown
The columns to know: LOAD (did the unit file parse), ACTIVE and SUB (the high- and low-level state — active/running, or active/exited for a one-shot that did its job and left), and the description.
The dependency graph from the section above, made visible (a server boots to multi-user.target, so ask about that):
systemctl list-dependencies multi-user.target
multi-user.target
● ├─cron.service
● ├─dbus.service
● ├─rsyslog.service
● ├─ssh.service
● ├─systemd-logind.service
● ├─systemd-networkd.service
● ├─systemd-user-sessions.service
● ├─unattended-upgrades.service
● └─basic.target
● ├─-.mount
● ├─tmp.mount
● ├─paths.target
● ├─slices.target
● │ ├─-.slice
● │ └─system.slice
● └─sockets.target
● ├─dbus.socket
● ├─systemd-journald.socket
● └─systemd-networkd.socket
This is the tree systemd computed — read it as "to reach this target, all of these must be satisfied," indented by depth. Note sockets.target near the bottom: those are the listening sockets opened before their daemons, exactly the socket-activation trick from earlier.
And the one that answers "why is my boot slow":
systemd-analyze && systemd-analyze blame
Startup finished in 1.358s (kernel) + 8.405s (userspace) = 9.764s
4.867s fstrim.service
4.246s cloud-init-local.service
1.154s docker.service
1.004s apt-daily.service
999ms apt-daily-upgrade.service
884ms cloud-init-main.service
613ms dev-sda1.device
345ms apparmor.service
315ms man-db.service
165ms systemd-udev-trigger.service
The first line splits total boot time across kernel and userspace; blame then ranks every unit by how long it took to start. The worst offender is usually one service waiting on a network or a disk — here it's fstrim trimming the SSD — and now you can see it instead of guessing.
Cheat Sheet
# Asking about state
systemctl status nginx # health, PID, cgroup, recent logs — your go-to
systemctl is-active nginx # just "active" or "failed" (scriptable)
systemctl is-enabled nginx # will it start on boot?
systemctl list-units --type=service # everything running now
systemctl --failed # only the units that failed — check this first
# Controlling a service
systemctl start nginx # start now (this boot only)
systemctl stop nginx # stop now
systemctl restart nginx # stop then start
systemctl reload nginx # re-read config without dropping connections (if supported)
systemctl enable nginx # start automatically on every boot
systemctl enable --now nginx # enable AND start, in one go
systemctl disable nginx # don't start on boot
# Editing units
systemctl edit nginx # safe drop-in override in /etc/systemd/system
systemctl cat nginx # show the effective unit file + any overrides
systemctl daemon-reload # re-read unit files after editing by hand
# The logs (journald)
journalctl -u nginx # all log lines for this unit, across restarts
journalctl -u nginx -e # ...jumped to the newest
journalctl -p err -b # only errors, only this boot
journalctl -f # follow live, like tail -f for the whole system
# Boot and state
systemctl get-default # which target boots by default
systemctl set-default multi-user.target # boot to server mode, no desktop
systemd-analyze blame # what made the boot slow, ranked
History and Philosophy
systemd arrived in 2010 from Lennart Poettering and Kay Sievers, and it arrived swinging. The "Rethinking PID 1" essay didn't propose a gentle improvement; it argued the entire shell-script model of booting was wrong, and offered a replacement that did parallel startup, socket activation, and cgroup tracking out of the box. Fedora shipped it by default in 2011, Arch in 2012, and after a famously bitter debate the Debian project chose it in 2014, with Ubuntu following in 2015. Within five years the argument was over by adoption: nearly every major distribution runs systemd today.
But the argument over taste never ended, and it's worth understanding because it's the most interesting fight in modern Linux. The classic Unix philosophy is "write programs that do one thing and do it well" — small, sharp tools you compose. An init system, the critics say, should init and nothing else. systemd does init, and logging, and network configuration, and device management, and login sessions, and timers, and DNS resolution, and more — a whole confederation of programs under one name. To its detractors (Eric Raymond among the louder ones) that's feature creep: a monolith swallowing jobs that belonged to separate tools, growing the attack surface and tying everything to one project's choices.
To its defenders, the uniformity is the point. The reason every service used to need its own fragile start-up script was precisely that there was no shared framework — so everyone reinvented process tracking, badly. A reader who's followed this page can see the case for the defense in concrete terms: the cgroup tracking is genuinely better than pidfiles, socket activation genuinely parallelises boot, and one systemctl genuinely beats a hundred bespoke init scripts. Whether that's worth the loss of small, swappable parts is a real question of engineering taste, and honest people still land on both sides. The healthy thing is to know why it works the way it does — which is the whole reason this page is longer than "type systemctl start."
See Also
- init system — what PID 1 fundamentally is, and how it adopts orphaned processes
systemctl— the command that drives all of systemd, flag by flagjournalctl— reading the structured logs journald collects- cgroup — the kernel feature systemd uses to track and cap every service
cron— the older scheduler that systemd timers can replace/etc/fstab— the mount table systemd turns into mount units- the kernel — what hands control to systemd once the hardware is ready
- service won't start — diagnosing a failed unit with status and journalctl
Is your server actually running everything it's supposed to be?
CleverUptime watches the services systemd is babysitting and tells you the moment one stops coming back — in plain language, with the fix, not just a red dot.
Want to see your own server's health right now? One command, no signup, no install.