systemd Overview
systemd is the dominant init and service manager on modern Linux. Per
systemd(1), it “is a system and service manager for Linux operating systems. When run as first process on boot (as PID 1), it acts as init system that brings up and maintains userspace services. Separate instances are started for logged-in users to start their services.” Where the oldsysvinitran a fixed sequence of shell scripts one after another, systemd models the whole system as a graph of units — services, sockets, targets, mounts, timers, slices, scopes, devices, paths — and brings them up in parallel, constrained only by explicit dependency and ordering declarations. Every service it starts is supervised inside its own cgroup, so the kernel — not fragile PID-file tracking — is the source of truth for which processes belong to a service. Startup is made fast and lazy by activation: a unit can be triggered on demand by an incoming socket connection, a D-Bus message, a device appearing, a timer firing, or a filesystem path changing. This note frames the systemd model and why it won; its components are deep-dived in sibling notes.
Versions pinned in this note (as of 2026-09-11)
systemd is a separate upstream project from the Linux kernel and versions independently of it — a single integer, bumped roughly quarterly, with no
major.minorsplit. Verified against the project’s own release tags via the GitHub API: v258 was tagged 2025-09-17, v259 2025-12-17, v260 2026-03-17, and v261 — the current stable major release — 2026-06-19. v262 is not out yet: the newest tags arev262-rc1(2026-09-01) andv262-rc2(2026-09-08). Source citations below are read at tagv259, which is what the Fedora 44 machine used for the live examples actually runs (systemd 259 (259.8-1.fc44)); behaviour differences in v260/v261 are called out where they exist. Do not pin a kernel version such as 6.12 for systemd facts — the two projects share nothing but an ABI.
This is the orienting note for systemd; it deliberately stays at the model level. The unit abstraction itself is systemd Units, the long-running-process unit is systemd Service Units, the boot synchronization points are systemd Targets and the Boot Sequence, dependency/ordering semantics are systemd Dependencies and Ordering, the cgroup integration is systemd and cgroup Integration, lazy startup is systemd Socket Activation, and logging is The systemd Journal. What makes PID 1 special to the kernel is PID 1 and the init Process; the head-to-head against sysvinit/OpenRC is The Init System Comparison.
Mental Model
Stop thinking of init as a script and start thinking of it as a dependency-resolving graph engine with a supervisor attached. You declare what you want (units), and which units need which others (dependencies) and in what order they may start (ordering) — and systemd computes a valid parallel schedule and executes it, then keeps every started process alive in a cgroup it can monitor and restart. The boot “goal” is itself just a unit: default.target, a node with no behaviour of its own that simply pulls in everything that should be running. Reaching a usable system is reaching a target in the graph.
flowchart TB PID1["systemd (PID 1)<br/>graph engine + supervisor"] DEF["default.target<br/>(alias: multi-user / graphical)"] SI["sysinit.target"] BAS["basic.target"] MU["multi-user.target"] S1["sshd.service<br/>(own cgroup)"] S2["nginx.service<br/>(own cgroup)"] SK["nginx.socket<br/>(socket activation)"] TM["logrotate.timer"] PID1 --> DEF DEF --> MU MU --> BAS BAS --> SI MU --> S1 MU --> SK SK -. "connection arrives" .-> S2 BAS --> TM TM -. "fires" .-> X["logrotate.service"]
The systemd model as a graph reached at boot. What it shows: PID 1 activates default.target, which depends (transitively, through multi-user → basic → sysinit) on the services that constitute a running system; each service runs in its own cgroup; and some units (nginx.socket, logrotate.timer) exist to activate other units lazily — the socket starts nginx.service only when a connection arrives, the timer starts logrotate.service only when it fires. The insight to take: “booting” is just resolving and reaching a target node in a dependency graph, and much of what runs does so on demand rather than eagerly at boot — which is how parallelism and fast boots are achieved.
What a unit is, and the unit zoo
A unit is systemd’s atom of management — a named, typed object with a .<type> suffix, configured by a declarative INI-style unit file, that systemd can start, stop, and track. systemd(1) enumerates the unit types; each exists because systemd manages a fundamentally different kind of resource:
- Service (
.service) — “start and control daemons and the processes they consist of.” The workhorse: a long-running program, supervised, restartable. - Socket (
.socket) — “encapsulate local IPC or network sockets… useful for socket-based activation.” systemd holds the listening socket and starts the service on first connection. - Target (
.target) — “useful to group units, or provide well-known synchronization points during boot-up.” Behaviourless grouping nodes; the replacement for runlevels. - Device (
.device) — “expose kernel devices in systemd… may be used to implement device-based activation.” Units appear/disappear asudevdiscovers hardware. - Mount (
.mount) / Automount (.automount) — “control mount points” and “provide automount capabilities, for on-demand mounting.” systemd owns/etc/fstabmounting and lazy mounting. - Timer (
.timer) — “useful for triggering activation of other units based on timers.” The cron replacement. - Swap (
.swap) — “encapsulate memory swap partitions or files.” - Path (
.path) — “activate other services when file system objects change or are modified.”inotify-driven activation. - Slice (
.slice) — “group units which manage system processes in a hierarchical tree for resource management.” The cgroup tree’s branch nodes. - Scope (
.scope) — “manage foreign processes instead of starting them.” Wraps processes systemd did not fork (e.g. a user session, a container) so they still live in a managed cgroup.
Laid out as a grid, the zoo stops looking arbitrary — each row answers “what resource does systemd own here, and who creates the unit?” The rightmost column is a live census from the Fedora 44 machine used throughout this note (systemctl list-units --all, 718 units total, 2026-09-11):
| Unit type | Manages | Usually created by | Can activate other units? | Holds processes in a cgroup? | Live count on the example box |
|---|---|---|---|---|---|
.service | a daemon or one-shot program and its processes | a packaged or admin-written unit file | no (it is the thing activated) | yes (leaf cgroup) | 235 |
.socket | a listening AF_UNIX/AF_INET/netlink socket or FIFO held open by PID 1 | unit file, paired with a .service | yes — on first connection | no (PID 1 holds the fd) | 70 |
.target | nothing at all — a named synchronisation point | unit file; also generated for legacy runlevels | indirectly, by pulling units in | no | 86 |
.device | a kernel device exposed as a unit | generated automatically from udev when a device is tagged systemd | yes — device-based activation | no | 246 |
.mount | a mount point | generated from /etc/fstab by systemd-fstab-generator, or written by hand | no | no | 44 |
.automount | an autofs mount point that mounts on first access | unit file or x-systemd.automount in fstab | yes — triggers its .mount | no | 2 |
.swap | a swap partition or file | generated from /etc/fstab | no | no | 4 |
.timer | a monotonic or wall-clock schedule | unit file, paired with a .service | yes — on expiry | no | 7 |
.path | an inotify watch on a filesystem object | unit file, paired with a .service | yes — on the watched change | no | 4 |
.slice | a branch of the cgroup tree, for resource apportionment | unit file, or implied by naming (system.slice) | no | branch only — no processes of its own | 14 |
.scope | processes systemd did not fork (a login session, a container, a systemd-run --scope job) | created at runtime over D-Bus, never from a file on disk | no | yes (leaf cgroup) | 6 |
Table 1 — the eleven unit types. What it shows: three distinct groupings hide behind one list. Types that hold processes (.service, .scope) versus types that group them (.slice) versus types that hold no processes at all. Types written by humans versus types generated — note that the largest single population on a real system, .device at 246, is entirely machine-generated from udev and never appears in /etc/systemd/system. And five of the eleven exist chiefly to activate something else. The insight to take: “unit” is not a synonym for “service.” Two thirds of the units on a running system are bookkeeping for resources systemd tracks but does not run.
The slice/scope/service split is the cgroup story: services and scopes are leaf cgroups that hold processes, slices are branch cgroups that group and apportion resources. systemd builds a tree with -.slice at the root, then system.slice (system services), user.slice (per-user sessions), and machine.slice (containers/VMs) beneath it — “Processes systemd spawns are placed in individual Linux control groups named after the unit which they belong to in the private systemd hierarchy” (systemd(1)). The mechanics and resource knobs are systemd and cgroup Integration.
How startup actually works
When the kernel hands control to userspace, it execs PID 1 — “systemd is usually not invoked directly by the user, but is installed as the /sbin/init symlink” (init(1)). On startup systemd reads unit files from a search path, then — “On boot systemd activates the target unit default.target whose job is to activate on-boot services and other on-boot units by pulling them in via dependencies” (systemd(1)). default.target is normally a symlink to either multi-user.target (text/server) or graphical.target (desktop). The transitive dependency chain documented in bootup(7) flows local-fs.target → sysinit.target → basic.target → multi-user.target → (graphical.target), with display-manager.service or getty@.service at the leaf producing a login prompt — the target chain is systemd Targets and the Boot Sequence and The systemd Target Ordering sysinit basic multi-user.
The pivotal scheduling rule — the thing that makes systemd parallel — is that requirement and ordering are independent axes. systemd(1): “If only a requirement dependency exists between two units (e.g. foo.service requires bar.service), but no ordering dependency (e.g. foo.service after bar.service) and both are requested to start, they will be started in parallel.” You declare that you need something with Requires=/Wants= and when you need it with After=/Before= — and systemd parallelizes everything not explicitly ordered. The semantics (Requires vs Wants vs Requisite vs BindsTo; After vs Before; Conflicts) are systemd Dependencies and Ordering.
The two axes, and why conflating them is the #1 unit-file bug
systemd.unit(5) states the orthogonality in as many words, in the Before=, After= entry: “Note that those settings are independent of and orthogonal to the requirement dependencies as configured by Requires=, Wants=, Requisite=, or BindsTo=.” The clearest way to internalise it is a 2×2 of the two axes:
| No ordering declared | After=b.service declared | |
|---|---|---|
| No requirement declared | b is not pulled in and is not waited for. If something else happens to start b, the two run concurrently. Effectively: no relationship. | b is still not started. But if b is in the same transaction for some other reason, a waits for it. This is the trap: After= alone looks like a dependency and pulls in nothing. |
Wants=b.service | b is started, simultaneously with a, with no delay between them (the man page’s own wording). a starts even if b fails. | The recommended idiom. b is pulled in, a waits for b to finish starting, and a still starts if b failed. |
Requires=b.service | b is started concurrently with a. Surprising consequence: because there is no ordering, b failing does not stop a from starting — the man page says the abort only happens “if… an ordering dependency After= on the failing unit is set.” | Strict: b is pulled in, a waits, and if b fails to activate a is not started. Also a is stopped/restarted whenever b is explicitly stopped/restarted. |
Table 2 — requirement (rows) against ordering (columns), each cell describing the actual resulting behaviour per systemd.unit(5). What it shows: the two axes really are independent — all six cells are reachable and mean different things. The insight to take: the middle-left cell is where most real bugs live. Requires=b.service without After=b.service does not give you “start b first, and fail if it breaks”; it gives you “start b at the same time, and proceed regardless.” The man page is explicit that the failure propagation is conditional on the ordering dependency existing. Almost every correct unit names the same unit in both an ordering and a requirement directive — as the man page puts it, “It is a common pattern to include a unit name in both the After= and Wants= options.”
With that established, the full requirement vocabulary is a spectrum of strength rather than a set of alternatives:
| Directive | Strength | Pulls the listed unit in? | If the listed unit fails to start | If the listed unit is explicitly stopped | If the listed unit dies unexpectedly | Implies ordering? | Inverse (auto-created on the target) | Added in |
|---|---|---|---|---|---|---|---|---|
Wants= | weakest | yes | this unit starts anyway | nothing | nothing | no | WantedBy= | v201 |
Requires= | strong | yes | this unit fails — only if After= is also set | this unit is stopped/restarted | nothing (a service exiting cleanly does not propagate) | no | RequiredBy= | v201 |
Requisite= | strong, non-pulling | no — if not already active, this unit fails immediately | n/a | n/a | nothing | no (pair it with After=) | RequisiteOf= (not settable directly) | v201 |
BindsTo= | strongest | yes | this unit fails | this unit is stopped/restarted | this unit is stopped too; with After=, also when the unit is skipped by a failed Condition*= | no (the docs say “in many cases it is best to combine BindsTo= with After=”) | BoundBy= (not settable directly) | v201 |
PartOf= | one-way, stop/restart only | no | nothing | this unit is stopped/restarted | nothing | no | ConsistsOf= (not settable directly) | v201 |
Upholds= | continuous | yes, and keeps restarting it whenever found inactive or failed | keeps retrying (rate-limited) | restarts it | restarts it | no | UpheldBy= | v249 |
Conflicts= | negative | starting this stops the listed unit, and vice versa | n/a | n/a | n/a | no — needs an explicit After=/Before= | ConflictedBy= | v201 |
After= / Before= | ordering only | no | nothing | nothing | nothing | yes, by definition | each other | v201 |
Table 3 — the requirement directives, from systemd.unit(5) (the “Added in version” column is the man page’s own). What it shows: four distinctions the words alone do not convey — whether the directive pulls the unit in (Requisite= and PartOf= do not), whether it reacts to an unexpected death as opposed to an administrator-issued stop (only BindsTo= does), whether it is one-way (PartOf=), and whether it is one-shot or continuous (Wants= fires once at start; Upholds= keeps firing forever). The insight to take: upstream’s own advice is to reach for the weak one — “Often, it is a better choice to use Wants= instead of Requires= in order to achieve a system that is more robust when dealing with failing services.” Strictness is not a virtue in a dependency graph; it is a way to turn one broken unit into a failed boot.
One further asymmetry worth naming, because it explains why shutdown ordering rarely needs to be written by hand: ordering is automatically inverted on stop. Per the man page, “When two units with an ordering dependency between them are shut down, the inverse of the start-up order is applied,” and when one unit is stopping while another starts, “the shutdown is ordered before the start-up in all cases” regardless of which direction the ordering was written. This is also why Conflicts= plus either After= or Before= is sufficient to guarantee the conflicting unit is fully stopped before its replacement starts.
Jobs and transactions — the actual graph engine
“systemd computes a valid parallel schedule” is the hand-wave; the mechanism is a two-level model that is worth naming because every confusing systemd error message (“Found ordering cycle”, “Transaction is destructive”, “job type not applicable”) comes from it.
A unit is durable state. A job is a pending change to one unit — src/core/job.h at v259 defines the types as JOB_START, JOB_VERIFY_ACTIVE, JOB_STOP, JOB_RELOAD, JOB_RESTART, JOB_NOP, plus three that collapse before entering (JOB_TRY_RESTART, JOB_TRY_RELOAD, JOB_RELOAD_OR_START). A transaction is a set of jobs that must be installed atomically. When you run systemctl start foo.service, systemd does not enqueue one job: it builds a transaction whose anchor job is start foo.service, recursively walks foo’s requirement dependencies adding more jobs (transaction_add_job_and_dependencies()), and then runs that whole set through a ten-step pipeline in transaction_activate() before anything executes.
flowchart TB REQ["systemctl start foo.service<br/>(or: boot -> start default.target)"] --> ANCHOR["Create ANCHOR job<br/>JOB_START foo.service"] ANCHOR --> WALK["transaction_add_job_and_dependencies():<br/>recursively add jobs for<br/>Requires / Wants / BindsTo / Conflicts"] WALK --> S1["1. transaction_find_jobs_that_matter_to_anchor()<br/>mark which jobs the request actually needs"] S1 --> S2["2. transaction_minimize_impact()<br/>avoid stopping running services if possible"] S2 --> S3["3. transaction_drop_redundant()<br/>discard jobs that change nothing"] S3 --> S4["4. transaction_collect_garbage()<br/>drop jobs nothing depends on"] S4 --> S5{"5. transaction_verify_order()<br/>any ordering cycle?"} S5 -- "cycle, and some job in it<br/>does NOT matter to the anchor" --> BREAK["Delete that job:<br/>'Found ordering cycle'<br/>+ 'SKIP: Ordering cycle found, skipping X'<br/>-> loop back to step 4"] BREAK --> S4 S5 -- "cycle, all jobs matter" --> FAIL1["FAIL: 'Transaction order is cyclic'<br/>nothing is started"] S5 -- "acyclic" --> S6["6. transaction_merge_jobs()<br/>merge compatible job types on one unit"] S6 -- "unmergeable job dropped" --> S4 S6 --> S8["8. drop_redundant() again<br/>(merging may have freed more)"] S8 --> S9{"9. transaction_is_destructive()?<br/>does this contradict running jobs?"} S9 -- "yes, and mode forbids it" --> FAIL2["FAIL: 'Requested transaction<br/>contradicts existing jobs'"] S9 -- "no" --> S10["10. transaction_apply()<br/>install jobs into manager->jobs;<br/>execution begins"] S10 --> RUN["Jobs run in parallel,<br/>constrained ONLY by After=/Before= edges"]
The transaction pipeline, step-numbered from the comments in src/core/transaction.c at systemd v259. What it shows: the ordering graph is checked, repaired and merged before a single process is forked — systemd either installs a consistent set of jobs or refuses. Note the two loop-backs: breaking a cycle and dropping an unmergeable job both invalidate earlier steps, so the pipeline re-runs garbage collection. The insight to take: an ordering cycle is not always fatal. systemd tries to break it by deleting a job that job_matters_to_anchor() says the original request does not actually need — which is why you sometimes see Ordering cycle found, skipping <unit> and the boot proceeds with one service silently missing. That log line is a warning about a latent misconfiguration, not a transient glitch, and it should be fixed rather than ignored.
The consequence for reading unit files: Requires= and friends determine which jobs enter the transaction, while After=/Before= determine only the order the installed jobs may execute in. They act at two different stages of the same pipeline. That is the mechanical reason Table 2’s two axes are orthogonal — it is not a stylistic choice, it is where in transaction_activate() each directive is consulted.
The unit state machine
Every unit, whatever its type, reports one of eight active states — the UnitActiveState enum in src/basic/unit-def.h at v259:
stateDiagram-v2 [*] --> inactive: unit loaded inactive --> activating: start job runs activating --> active: ExecStart succeeded<br/>(or sd_notify READY=1<br/>for Type=notify) activating --> failed: ExecStart failed<br/>or start timeout active --> deactivating: stop job runs<br/>(or main process exits) deactivating --> inactive: stopped cleanly deactivating --> failed: nonzero exit / signal /<br/>stop timeout active --> reloading: systemctl reload reloading --> active: reload finished active --> refreshing: live image/resource refresh refreshing --> active: refresh finished failed --> activating: Restart= policy fires,<br/>or systemctl restart inactive --> maintenance: administrative hold maintenance --> inactive failed --> inactive: systemctl reset-failed
The UnitActiveState machine. What it shows: the eight enum values (UNIT_ACTIVE, UNIT_RELOADING, UNIT_INACTIVE, UNIT_FAILED, UNIT_ACTIVATING, UNIT_DEACTIVATING, UNIT_MAINTENANCE, UNIT_REFRESHING) and the transitions between them. The insight to take: failed is a sticky state, not a transient one — a unit that has failed stays failed and continues to be reported as such until either a Restart= policy moves it back through activating or an administrator runs systemctl reset-failed. This is why systemctl --failed is a useful health check and why “I restarted it and it’s fine now” does not clear the record on its own. The SubState you see in systemctl status (running, exited, dead, auto-restart, …) is a per-unit-type refinement underneath these eight generic states; unit.h groups them with UNIT_IS_ACTIVE_OR_RELOADING() (active, activating, reloading, refreshing) and UNIT_IS_INACTIVE_OR_FAILED() (inactive, failed, deactivating).
Unit file search path and precedence
Unit files are read from a layered set of directories. The rule is stated once in systemd.unit(5) and applies uniformly: “Unit files found in directories listed earlier override files with the same name in directories lower in the list.” The full system-mode list is ten entries deep, and the shape of it is more interesting than the paths themselves:
| Rank | Path | What lives there | Who writes it |
|---|---|---|---|
| 1 | /etc/systemd/system.control/ | persistent config created over the D-Bus API | systemctl set-property, tooling |
| 2 | /run/systemd/system.control/ | the same, non-persistent | tooling |
| 3 | /run/systemd/transient/ | transient units | systemd-run |
| 4 | /run/systemd/generator.early/ | generated units, high priority | generators |
| 5 | /etc/systemd/system/ | units created by the administrator | you |
| 6 | /run/systemd/system/ | runtime units | anything, at runtime |
| 7 | /run/systemd/generator/ | generated units, medium priority | generators |
| 8 | /usr/local/lib/systemd/system/ | units installed by the administrator (not packaged) | you, make install |
| 9 | /usr/lib/systemd/system/ | units installed by the distribution package manager | RPM/dpkg |
| 10 | /run/systemd/generator.late/ | generated units, low priority | generators |
Table 5 — the system-mode unit load path, highest precedence first, from systemd.unit(5). What it shows: three interleaved concerns, not one ladder. Reading down, you pass API-created config (1–3), then generator output (4), then human-authored config (5), then more runtime and generator output (6–7), then installed packages (8–9), then generator output again (10). The insight to take: generators appear at three different priorities on purpose — generator.early beats even /etc, generator.late loses to everything. That is how systemd-fstab-generator can synthesise a .mount unit that an administrator is still able to override, while a generator that only wants to supply a fallback can place itself below every hand-written file. A parallel, longer table exists for --user mode, keyed off the XDG base directory variables.
A unit present in /etc shadows the same-named unit in /usr/lib, which is how an administrator overrides a packaged service without editing package-owned files. But wholesale shadowing is the blunt instrument; the sharper one is a drop-in: a *.conf fragment placed in /etc/systemd/system/<unit>.d/ which is merged on top of the original rather than replacing it. Two properties of drop-ins are routinely misunderstood, and the man page spells both out:
- List-valued settings accumulate, they do not replace. Writing
ExecStart=/new/binaryin a drop-in gives a service with twoExecStart=lines, not one. To replace, you must clear first: an emptyExecStart=on its own line, then the new value. The man page: “if one wants to remove entries from a setting that is parsed as a list… one needs to first clear the list before re-adding all entries except the one that is to be removed.” - Dependencies cannot be removed by a drop-in at all. “Dependencies (
After=, etc.) cannot be reset to an empty list, so dependencies can only be added in drop-ins. If you want to remove dependencies, you have to override the entire unit.” This is the single most common reason a drop-in “doesn’t work”: you cannot delete anAfter=edge this way, only add one, and the fix is a full unit override (systemctl edit --full).
A related facility is the top-level drop-in: a directory named for a unit type rather than a unit, e.g. /etc/systemd/system/service.d/, whose contents apply to every service unit on the system. Useful for fleet-wide policy; also an easy way to break everything at once.
Activation — lazy, parallel startup
The deepest idea systemd brought from its origin (Lennart Poettering’s “Rethinking PID 1”) is activation: rather than starting every daemon eagerly and serially, create the interfaces daemons talk through and start the daemons only when something uses them. The five activation triggers are all unit types: socket-based (a .socket unit holds the listening socket; the service starts on first connection), bus-based (a D-Bus name request triggers the service), device-based (a .device appears via udev), path-based (a .path watches a filesystem object via inotify), and timer-based (a .timer). Socket activation is the load-bearing one and is its own note, systemd Socket Activation. As Poettering put it, “if we manage to make those sockets available for connection earlier and only actually wait for that instead of the full daemon start-up, then we can speed up the entire boot”, because “the kernel socket buffers help us to maximize parallelization, and the ordering and synchronization is done by the kernel, without any further management from userspace.”
The idea is not systemd’s invention — Poettering’s socket-activation essay traces it to inetd (which spawned one instance per connection, hence its reputation for slowness, though its wait mode could spawn one instance for all subsequent connections) and then to Apple’s launchd, which made the leap of applying it to AF_UNIX sockets and to services that need to start at boot anyway — “probably the main reason why MacOS is so fast booting up.”
The essay’s worked example is the clearest statement of what this buys, and it is worth walking because it is what “parallel boot” actually means. Take four services with a real dependency chain: D-Bus logs to syslog; Avahi needs both syslog and D-Bus; Bluetooth also needs syslog and D-Bus but is independent of Avahi. On SysV that serialises as Syslog → D-Bus → Avahi → Bluetooth. Parallelising only the independent pair gets you Syslog → D-Bus → {Avahi, Bluetooth}. Socket activation collapses it entirely: “in a single step the /dev/log and /run/dbus/system_bus_socket sockets are created, and in the next step all four services are spawned simultaneously.”
sequenceDiagram autonumber participant P1 as systemd (PID 1) participant SK as nginx.socket participant K as kernel socket buffer participant CL as client (curl) participant SV as nginx.service Note over P1,SK: at boot, before any daemon runs P1->>SK: start nginx.socket SK->>K: socket() + bind() + listen() on :80 Note over P1,K: PID 1 now OWNS the listening fd.<br/>The socket is connectable.<br/>nginx.service has NOT been started. CL->>K: connect() to :80 — succeeds immediately CL->>K: write("GET / HTTP/1.1") — buffered by the kernel Note over CL: client is NOT blocked and sees no error —<br/>the kernel queues the connection K-->>P1: epoll: fd readable P1->>SV: start nginx.service (transaction as usual) P1->>SV: fork+exec with the listening fd as fd 3,<br/>LISTEN_FDS=1, LISTEN_PID set to the child pid SV->>SV: sd_listen_fds() returns 1, so<br/>fd = SD_LISTEN_FDS_START + 0 SV->>K: accept() — gets the queued connection SV-->>CL: HTTP response Note over P1,SV: If nginx.service later crashes and restarts,<br/>PID 1 still holds the listening fd:<br/>connections queue instead of being refused.
Socket activation as a message sequence. What it shows: the listening socket’s lifetime is decoupled from the daemon’s. PID 1 creates and holds it early; the client’s connect() succeeds against a socket no server has ever accept()ed on; the kernel’s backlog absorbs the gap; the daemon inherits the already-bound fd as file descriptor 3 (SD_LISTEN_FDS_START is defined as 3) with LISTEN_FDS and LISTEN_PID set in its environment. The insight to take: this is what makes ordering unnecessary rather than merely fast. A client never has to be told “wait until the server is up,” because from the client’s point of view the server was always up. The same property gives you zero-downtime restarts for free — during a service restart the socket stays bound, so connections queue rather than getting ECONNREFUSED. The cost is a real API contract: the daemon must be written (or patched) to accept an inherited fd instead of binding its own.
Supervision and logging
Every service systemd starts is a child it wait()s on and tracks via the service’s cgroup, so it knows the moment a service dies and can apply a restart policy. Service output (stdout/stderr) and structured log records are captured by journald (systemd-journald), systemd’s logging daemon — “a journal log record is generated declaring the consumed resources whenever a unit shuts down” (systemd(1)). The journal is structured, indexed, and queryable per-unit (journalctl -u nginx); it is The systemd Journal. PID 1 itself is controllable at runtime over its D-Bus API (org.freedesktop.systemd1) — “The D-Bus API of systemd is described in org.freedesktop.systemd1(5)” — which is what systemctl speaks (see systemctl and Unit Management), and via signals: SIGTERM to PID 1 triggers a daemon-reexec, SIGINT triggers ctrl-alt-del.target, and a family of SIGRTMIN+n signals request specific shutdown/reboot/log-level modes (systemd(1)).
A minimal unit file
# /etc/systemd/system/myapp.service
[Unit]
Description=My example service
After=network-online.target # ordering: don't start until the network is up
Wants=network-online.target # requirement (weak): pull the target in, but
# don't fail if it can't be reached
[Service]
Type=notify # the daemon calls sd_notify(READY=1) when ready;
# systemd waits for that before considering it started
ExecStart=/usr/bin/myapp --serve
Restart=on-failure # supervision: relaunch if it exits non-zero
RestartSec=2s
MemoryMax=512M # cgroup resource limit, enforced by the kernel
[Install]
WantedBy=multi-user.target # `systemctl enable` makes multi-user.target Want thisLine by line: the [Unit] section declares relationships — After= is ordering only (start after the network target is reached) while Wants= is a weak requirement (try to bring the target up, but don’t abort if it fails); the two together are the common idiom for “I need the network, but I won’t die if it’s flaky.” [Service] defines the process: Type=notify means systemd holds the unit in “activating” until the daemon explicitly signals readiness via the sd_notify protocol (so dependents ordered After=myapp.service start only once it is truly ready, not merely forked); Restart=on-failure is the supervision policy; MemoryMax=512M becomes a cgroup memory.max limit the kernel enforces. [Install] is not read at runtime — it is consulted by systemctl enable, which creates the symlink that makes multi-user.target Want this unit, i.e. start it at boot. Service-unit specifics (Type=, Exec*=, restart policy) are systemd Service Units.
The same service, socket-activated
Rewriting the example to use socket activation shows what actually changes — and how little of it is in the service file:
# /etc/systemd/system/myapp.socket <-- a SEPARATE unit, same basename
[Unit]
Description=Socket for My example service
[Socket]
ListenStream=8080 # PID 1 binds and listens on TCP/8080
Accept=no # ONE service instance gets the LISTENING fd
# (Accept=yes would spawn myapp@N.service
# per connection, inetd-style — rarely wanted)
[Install]
WantedBy=sockets.target # enable the SOCKET, not the service# /etc/systemd/system/myapp.service <-- the ordering/requirement edges GO AWAY
[Unit]
Description=My example service
# no After=network-online.target needed:
# the socket is already bound before we run
[Service]
Type=notify
ExecStart=/usr/bin/myapp --serve # myapp calls sd_listen_fds(); if it returns 1,
# it uses fd 3 instead of calling bind()
Restart=on-failure
MemoryMax=512M
# no [Install] section at all — this unit is
# never enabled; the socket starts itFour things moved. (1) systemctl enable myapp.socket replaces systemctl enable myapp.service — enabling the service would defeat the point by starting it eagerly. (2) The After=/Wants=network-online.target pair is deleted: clients cannot fail to connect while the daemon is starting, because PID 1 bound the socket long before. (3) Accept=no is the important knob and the non-obvious default to reach for — with Accept=yes systemd accept()s each connection itself and spawns a fresh templated instance per connection, which is the slow inetd behaviour Poettering’s essay describes; Accept=no hands the listening descriptor to one long-lived process. (4) The service gains a code requirement: it must call sd_listen_fds() and use fd 3 when told to. That is the honest cost of socket activation — it is not purely a configuration change.
The pairing is by basename, per systemd.socket(5): “The name of the .service unit is by default the same as the name of the .socket unit, but can be altered with the Service= option.” With Accept=no you need a matching myapp.service; with Accept=yes you need a template, myapp@.service, “from which services are instantiated for each incoming connection.” That is why systemctl show myapp.service reports TriggeredBy=myapp.socket with nothing written in the service file.
One sharp edge the man page flags and most tutorials omit: “No implicit WantedBy= or RequiredBy= dependency from the socket to the service is added. This means that the service may be started without the socket, in which case it must be able to open sockets by itself. To prevent this, an explicit Requires= dependency may be added.” A socket-activated service can therefore be started by hand — or pulled in by some other unit’s Wants= — with no LISTEN_FDS in its environment, and if the code assumed activation it will come up listening on nothing. Adding Requires=myapp.socket to the service file closes that hole. Details are systemd Socket Activation.
Why systemd replaced sysvinit
The displacement was not fashion; it was four concrete deficiencies of the SysV model, each of which systemd answers structurally. The canonical argument is “Rethinking PID 1”:
- Serial startup was slow.
sysvinitran/etc/rc?.dscripts essentially one after another. systemd parallelizes everything not explicitly ordered, and defers much of it via activation. - Shell scripts were wasteful. Poettering measured a typical boot invoking “grep at least 77 times. awk is called 92 times, cut 23 and sed 74” — each a fork+exec+library-load — observing “No other language but shell would do something like that.” Declarative unit files replace the scripts with a few
key=valuelines parsed once. - Process supervision was unreliable. A double-forking daemon escapes PID-file tracking; init could not reliably tell whether a service was alive. systemd puts every service in a cgroup, and “cgroup membership is securely inherited by child processes, they cannot escape” — so the kernel authoritatively reports the service’s whole process tree, enabling correct restart and clean shutdown.
- No real dependency model. SysV encoded order crudely via
S20/K80filename-number prefixes and fixed runlevels. systemd has an explicit dependency graph and replaces the fixed runlevels with targets (it maps the legacy numbers for compatibility — “2, 3, and 4 are equivalent to systemd.unit=multi-user.target; and 5 is equivalent to systemd.unit=graphical.target”, persystemd(1)). The comparison with sysvinit, OpenRC, runit, and s6 is The Init System Comparison.
The cgroup-v2-only direction, and the legacy removals
systemd has steadily forced the move to the cgroup v2 unified hierarchy and away from the legacy v1 (and “hybrid”) layout. The legacy and hybrid hierarchies were deprecated in systemd v256, and v258 (released 2025-09-17) removed them outright: per the v258 NEWS, “Support for cgroup v1 (‘legacy’ and ‘hybrid’ hierarchies) has been removed. cgroup v2 (‘unified’ hierarchy) will always be mounted during system bootup and systemd-nspawn container initialization.” The practical consequence is that on a contemporary systemd system, every resource-control knob a service, container, or Kubernetes node uses lives under the single unified /sys/fs/cgroup/ tree — the same primitive containers use. On the Fedora 44 box used for the live examples, stat -fc %T /sys/fs/cgroup returns cgroup2fs, i.e. the unified hierarchy and nothing else. The cgroup integration is systemd and cgroup Integration.
Correction: the current release is v261, not v262
An earlier revision of this note stated that “the current release is systemd v262 (2026-06-19)”. The date is right and the version is wrong: querying the project’s release tags directly (
api.github.com/repos/systemd/systemd/git/refs/tags/v261→ tagger date2026-06-19T18:09:08Z) shows 2026-06-19 is v261. v262 exists only asv262-rc1(2026-09-01) andv262-rc2(2026-09-08) as of 2026-09-11. Readingmain’sNEWSgives the in-development section heading, which is why “what doesmainsay” is an unreliable way to answer “what is released” for this project.
The other removal people miss: System V script support is gone
The cgroup-v1 removal gets the attention, but a second compatibility layer was removed one release later and matters more for anyone maintaining old packages. Per the v260 NEWS: “Support for System V service scripts has been removed. Please make sure to update your software now to include a native systemd unit file instead of a legacy System V script.” The three components deleted are named explicitly: systemd-rc-local-generator and rc-local.service, systemd-sysv-generator, and systemd-sysv-install (the hook systemctl enable/disable/is-enabled used to call out to the distribution’s SysV tooling).
The deprecation notice is a small case study in how long this took. Reading the NEWS file backwards, the same warning appears with the removal target slipping release by release: scheduled for v258, then “(postponed to v260) … will be removed in v259”, then in v259’s notes “will be removed in v260”, and finally executed in v260 (2026-03-17).
| systemd release | Date | Legacy support status |
|---|---|---|
| v256 | 2024-06-11 | cgroup v1 legacy/hybrid hierarchies deprecated; SysV scripts deprecated |
| v257 | 2024-12-10 | both still present, removal repeatedly postponed |
| v258 | 2025-09-17 | cgroup v1 removed — “cgroup v2 (‘unified’ hierarchy) will always be mounted during system bootup” |
| v259 | 2025-12-17 | SysV deprecation notice says removal in v260 |
| v260 | 2026-03-17 | SysV script support removed (systemd-sysv-generator deleted) |
| v261 | 2026-06-19 | current stable major release |
| v262 | — | -rc2 as of 2026-09-08, not released |
Table 4 — the two compatibility removals, with dates taken from the project’s git tags rather than from changelog prose. The insight to take: a systemd version number is load-bearing information about what a system can still run. “Does this box still honour /etc/init.d/foo?” is answered by one integer, and the answer flipped at v260.
Distribution lag makes this concrete rather than academic. The Fedora 44 machine used here runs systemd 259, and systemctl --version prints its compile-time feature list including +SYSVINIT — so SysV generator support is still present on this box even though upstream has removed it, and will disappear when Fedora moves to v260 or later. Enterprise distributions lag much further.
Uncertain
Verify against the target distro: which systemd version a given 2026 distribution actually ships, and therefore whether cgroup v1 and SysV scripts are merely deprecated or fully removed there. Reason: upstream removal (v258 and v260 respectively) precedes distro adoption by quarters to years; I verified only one machine (Fedora 44 / systemd 259.8) directly. To resolve: run
systemctl --versionon the target and check it against Table 4. uncertain
Resolved: the freedesktop man pages are reachable again
A previous revision recorded that
freedesktop.org/software/systemd/man/latest/returned HTTP 403 and fell back to man7.org mirrors. Re-measured 2026-09-11 with plaincurland noUser-Agentoverride:systemd.unit.htmlreturns HTTP 200 / 179,777 bytes andsystemd.service.htmlHTTP 200 / 125,266 bytes, both real content. The dependency-directive quotations in Table 3 above are taken from the freedesktop page directly, which is the upstream-of-record.
Reading a real system
All of the above is checkable on any running machine in about five commands. Everything below is verbatim output from the Fedora 44 desktop used throughout this note — 32 logical CPUs, kernel 7.1.13-200.fc44, systemd 259 (259.8-1.fc44), captured 2026-09-11.
1. What version, with which features compiled in?
$ systemctl --version
systemd 259 (259.8-1.fc44)
+PAM +AUDIT +SELINUX -APPARMOR +IMA +IPE +SMACK +SECCOMP -GCRYPT +GNUTLS +OPENSSL +ACL
+BLKID +CURL +ELFUTILS +FIDO2 +IDN2 -IDN +KMOD +LIBCRYPTSETUP +LIBCRYPTSETUP_PLUGINS
+LIBFDISK +PCRE2 +PWQUALITY +P11KIT +QRENCODE +TPM2 +BZIP2 +LZ4 +XZ +ZLIB +ZSTD
+BPF_FRAMEWORK +BTF +XKBCOMMON +UTMP +SYSVINIT +LIBARCHIVEThe +/- list is compile-time feature flags, and it answers questions the version number alone cannot. +SYSVINIT here means this build still ships systemd-sysv-generator — consistent with v259 being one release before the removal (Table 4). -APPARMOR with +SELINUX tells you which MAC framework this build integrates with.
2. Where did boot time actually go?
$ systemd-analyze
Startup finished in 9.351s (firmware) + 4.509s (loader) + 2.453s (kernel)
+ 2min 23.036s (initrd) + 1min 5.956s (userspace) = 3min 45.307s
graphical.target reached after 1min 5.956s in userspace.Five phases, and only the last is systemd-as-PID-1. The 2 min 23 s initrd figure on this machine is a LUKS passphrase prompt — a human typing, not software. This is the first thing to establish before optimising anything: systemd-analyze will happily hand you a 3 min 45 s number of which 94% is not systemd’s doing.
3. blame — and why it lies
$ systemd-analyze blame | head -6
2min 23.614s sys-module-fuse.device
2min 23.545s sys-devices-platform-MSFT0101:00-tpm-tpm0.device
2min 23.545s dev-tpm0.device
2min 23.514s dev-tpmrm0.device
2min 23.514s sys-devices-platform-MSFT0101:00-tpmrm-tpmrm0.device
2min 23.513s dev-ttyS3.deviceEvery one of the top entries is a .device unit at ~2 min 23 s — the same number as the initrd phase. Nothing is wrong with the TPM or the serial ports, and these numbers are not even well-defined. systemd-analyze(1) says so outright: the command “shows the time units spent in ‘activating’ state, which is not defined for units such as device units that transition directly from ‘inactive’ to ‘active’. This command hence gives an impression of the performance of program code, but cannot accurately reflect latency introduced by waiting for hardware and similar events.” The man page adds a second, independent caveat — “systemd-analyze blame does not display results for services with Type=simple, because systemd considers such services to be started immediately, hence no measurement of the initialization delays can be done” — and a third: “the output might be misleading as the initialization of one service might be slow simply because it waits for the initialization of another service to complete.”
Put together: blame measures per-unit duration with no notion of the critical path, omits an entire common service type, and reports meaningless figures for device units. A unit can take 30 s and delay nothing, or take 200 ms and hold up the whole boot. It is a useful list, not a diagnosis.
4. critical-chain — the tool that answers the question you meant to ask
$ systemd-analyze critical-chain
The time when unit became active or started is printed after the "@" character.
The time the unit took to start is printed after the "+" character.
graphical.target @1min 5.956s
└─multi-user.target @1min 5.956s
└─docker.service @1min 3.466s +2.488s
└─network-online.target @1min 3.464s
└─network.target @3.458s
└─wpa_supplicant.service @3.452s +5ms
└─basic.target @2.745s
└─dbus-broker.service @2.725s +20ms
└─dbus.socket @2.698s +300us
└─sysinit.target @2.694s
└─systemd-vconsole-setup.service @1min 3.839s +511ms
└─systemd-journald.socketRead this from the bottom up: it is a chain of After= edges, each node annotated with when it became active (@) and how long it took (+). The real story is the network-online.target @1min 3.464s line — docker.service waits on the network being up, and that wait is what pushes graphical.target out to 1 min 6 s. The individual services are fast (dbus-broker 20 ms, wpa_supplicant 5 ms, dbus.socket 300 µs); the waiting is the cost.
Note also the deliberately confusing last line: systemd-vconsole-setup.service @1min 3.839s appears under sysinit.target @2.694s, i.e. a child that became active a minute after its parent. That is not a bug in the tool — critical-chain walks the ordering graph, and a unit can be re-triggered later (here by a device appearing) long after the target it is ordered before was reached. The man page’s own warning covers exactly this case: “the output might be misleading as the initialization of services might depend on socket activation and because of the parallel execution of units,” and like blame it “only takes into account the time units spent in ‘activating’ state.”
5. Ordering without requirement, in the wild
Table 2’s key claim is checkable on a real unit file. sshd.service as shipped by Fedora:
$ systemctl show sshd.service -p Requires -p Wants -p After
Requires=sysinit.target system.slice
Wants=ssh-host-keys-migration.service sshd-keygen.target
After=basic.target network.target system.slice ssh-host-keys-migration.service \
sysinit.target sshd-keygen.target systemd-journald.socketCompare the lists carefully. network.target and basic.target appear in After= and in neither Requires= nor Wants=. That is a deliberate, correct use of ordering-without-requirement: sshd does not want to cause the network stack to be brought up, but if it is being brought up in this transaction, sshd should start afterwards. Conversely sysinit.target and system.slice appear in both lists — the “common pattern” the man page recommends. And ssh-host-keys-migration.service appears in Wants= and After=: pull it in, and wait for it.
Contrast with docker.service on the same machine, which shows the strict form plus a pure-ordering edge:
$ systemctl show docker.service -p Requires -p Wants -p After -p TriggeredBy
Requires=docker.socket sysinit.target system.slice
Wants=containerd.service network-online.target
After=system.slice basic.target time-set.target containerd.service docker.socket \
firewalld.service nss-lookup.target network-online.target \
systemd-journald.socket sysinit.target
TriggeredBy=docker.socketfirewalld.service is in After= only — “if firewalld is starting, let it finish first; if it is not installed or not wanted, proceed.” network-online.target is in both Wants= and After=, which is why it showed up on the critical chain in step 4. And TriggeredBy=docker.socket is the automatically-created inverse of the socket unit’s Service= — evidence that docker is socket-activated here, not merely started at boot.
6. Supervision and the cgroup, live
$ systemctl show dbus-broker.service -p ControlGroup -p MemoryCurrent -p TasksCurrent \
-p Type -p ActiveState -p SubState -p NRestarts
ControlGroup=/system.slice/dbus-broker.service
MemoryCurrent=6643712
TasksCurrent=2
Type=notify-reload
ActiveState=active
SubState=running
NRestarts=0MemoryCurrent and TasksCurrent are not systemd’s own bookkeeping — they are reads of memory.current and pids.current in that cgroup. The kernel is the source of truth, exactly as the “why systemd replaced sysvinit” argument claims. Type=notify-reload — introduced in systemd v253, per that release’s NEWS entry “A new service type Type=notify-reload is defined” — is the sd_notify protocol extended to reloads: SIGHUP is sent, the service replies RELOADING=1 with a MONOTONIC_USEC= timestamp and then READY=1 when done, so systemctl reload waits for real completion rather than guessing.
$ systemd-cgls | head -8
CGroup /:
-.slice
├─user.slice
│ └─user-1000.slice
│ └─user@1000.service …
│ └─session.slice
│ └─dbus-broker.service
│ ├─6894 /usr/bin/dbus-broker-launch --scope userThe tree is the slice/scope/service model from Table 1 made visible: -.slice at the root, user.slice → user-1000.slice → user@1000.service (a per-user manager instance, a second systemd running as the user), and leaf service cgroups holding actual PIDs.
7. What is socket-activated right now
$ systemctl list-sockets | head -7
LISTEN UNIT ACTIVATES
@ISCSIADM_ABSTRACT_NAMESPACE iscsid.socket iscsid.service
@ISCSID_UIP_ABSTRACT_NAMESPACE iscsiuio.socket iscsiuio.service
audit 1 systemd-journald-audit.socket systemd-journald.service
kobject-uevent 1 systemd-udevd-kernel.socket systemd-udevd.service
/dev/rfkill systemd-rfkill.socket systemd-rfkill.service
/run/.heim_org.h5l.kcm-socket sssd-kcm.socket sssd-kcm.serviceSeventy .socket units on this box (Table 1), and the first six already show four different address families. @ISCSIADM_ABSTRACT_NAMESPACE is an abstract AF_UNIX socket (the leading @ means it has no filesystem path at all); audit 1 and kobject-uevent 1 are netlink sockets; /dev/rfkill is a character device systemd watches; /run/.heim_org.h5l.kcm-socket is a filesystem-backed AF_UNIX socket. Socket activation is not an IP-server feature — journald’s audit feed, udevd’s uevent feed and the rfkill switch are all held open by PID 1 by the same mechanism.
Failure Modes and Common Misunderstandings
“After= makes a unit a dependency.” It does not. After=/Before= are pure ordering — they say nothing about whether the other unit is started at all. You need Requires=/Wants=/BindsTo= for the requirement, and almost always both an ordering and a requirement together. Declaring only After=foo.service and expecting foo to be pulled in is the single most common unit-file bug. (systemd Dependencies and Ordering.)
Forgetting enable vs start. systemctl start runs a unit now; systemctl enable writes the [Install] symlinks so it starts at boot. They are independent — a started-but-not-enabled service vanishes on reboot, an enabled-but-not-started one does nothing until reboot.
Requires= without After= does not do what the name promises. This is Table 2’s middle-left cell and it deserves its own entry, because the mistake is the opposite of the previous one and is far less known. systemd.unit(5) on Requires=: “If one of the other units fails to activate, and an ordering dependency After= on the failing unit is set, this unit will not be started.” Without the After=, the two units start concurrently and the failure does not propagate to the start job at all. “Requires” reads like a guarantee; it is only half of one.
Requires= does not mean “must stay active.” A second trap in the same directive, spelled out in the man page: “this dependency type does not imply that the other unit always has to be in active state when this unit is running.” A service whose main process exits cleanly, or a device that gets unplugged, does not propagate to units that Requires= it. If you genuinely need “never active without X,” the documented combination is BindsTo= plus After=.
Type=simple is the worst choice for readiness, not a safe default. The man page is blunt: with Type=simple the manager “will consider the unit started immediately after the main service process has been forked off (i.e. immediately after fork(), and before various process attributes have been configured and in particular before the new process has called execve()).” A typo in ExecStart= therefore produces a unit that reports as successfully started. Upstream’s guidance is “It is recommended to use Type=exec for long-running services, as it ensures that process setup errors… are properly tracked”, and where the service can be modified, notify / notify-reload / dbus are “the preferred options as they allow service program code to precisely schedule when to consider the service started up successfully.” Type=forking remains the fallback for daemons that cannot be changed — “it supports the traditional heavy-weight UNIX service start-up protocol” — and should be paired with PIDFile=. Note the knock-on effect on diagnostics: systemd-analyze blame skips Type=simple services entirely, so the sloppiest service type is also the one you cannot profile.
“Ordering cycle found, skipping X” is a boot-time warning people scroll past. It means transaction_verify_order() detected a cycle and transaction_delete_unit() removed a job to break it — the boot proceeded with a service silently not started. The log carries structured fields DELETED_UNIT= and DELETED_TYPE= naming exactly what was dropped. Treat it as a configuration bug, not as noise, and find it with journalctl -b -p warning | grep -i 'ordering cycle'.
Reading systemd-analyze blame as a boot-time diagnosis. It is a duration list, not a critical path: it ignores queueing, omits Type=simple services, and reports undefined figures for device units that go straight from inactive to active. Use critical-chain for “what delayed the boot” and blame only to spot an individual unit that is unexpectedly slow. See the worked example above and systemd-analyze and Boot Profiling.
Thinking systemd “is just an init.” It is an init and a service supervisor, a cgroup resource manager, a socket/timer/path activator, a mount manager, and the hub of an ecosystem of helper daemons (below). Treating it as a drop-in /etc/rc.local runner misses the model entirely. Poettering’s own rebuttal essay, “The Biggest Myths”, is the primary source for which of the common criticisms the authors accept and which they reject.
Assuming legacy compatibility is still there. As of upstream v260 there is no systemd-sysv-generator, so /etc/init.d/ scripts are not units any more and rc-local.service is gone; as of v258 there is no cgroup v1. Both were present for over a decade and both are now simply absent. Check systemctl --version against Table 4 before assuming either still works.
The ancillary ecosystem (framed, not deep-dived)
“systemd” as shipped is a suite. PID 1 (the manager) is the core, but alongside it run a set of optional daemons that each own a slice of the platform — and which belong to other notes/MOCs, mentioned here only so the reader knows where the boundaries are: systemd-logind (login session and seat management — the user.slice/scope machinery), systemd-udevd (device-node management, feeding .device units), systemd-networkd (network configuration), systemd-resolved (DNS/nss), systemd-timesyncd (SNTP clock sync), and systemd-journald (logging, the one tightly bound to the manager — The systemd Journal). These are independently togglable; a minimal system runs PID 1 and journald and little else. The point of this note is the manager and its model; each helper is a sibling concern.
Alternatives and When to Choose Them
systemd is the default on Debian, Ubuntu, Fedora, RHEL, SUSE, and Arch — effectively the mainstream Linux desktop/server default. The alternatives persist in niches: sysvinit (legacy, still on some minimal systems), OpenRC (Gentoo/Alpine default; dependency-based but script-driven, lighter), runit and s6 (tiny, process-supervision-focused, popular in containers and minimalist distros like Void), and busybox init (embedded). The honest trade-off: systemd buys parallelism, robust supervision, socket activation, and a uniform management surface at the cost of size, complexity, and a large attack/feature surface as PID 1 — which is why minimal/embedded/container contexts often prefer the supervisors. The structured comparison is The Init System Comparison. Note that inside a container the relevant question is usually not “systemd vs sysvinit” but “do I even need an init?” — see Reaping Orphans and the subreaper for why a tiny reaping init (tini) is often the right PID 1 in a container rather than a full systemd.
Production Notes
systemd’s reach is the reason it is both loved and criticized in production. The upside is operational uniformity: systemctl status, journalctl -u, cgroup-enforced MemoryMax=/CPUQuota=, systemd-analyze blame/critical-chain for boot profiling (systemd-analyze and Boot Profiling), and socket activation that lets a host bring services up lazily — all the same regardless of the service. The cost is that PID 1 is now a large, central, fast-moving component; a systemd bug or misconfiguration is a system-wide event, and the cgroup-v2-only direction (v258+) has forced container runtimes and Kubernetes to follow (older v1-only tooling breaks). Container orchestrators that run systemd inside a container do so to get its supervision/cgroup model, but most container images instead run the app directly with a tiny reaping init — the two worlds meet at the cgroup substrate systemd and container runtimes both stand on (systemd and cgroup Integration, Linux Containers and Isolation MOC).
See Also
- PID 1 and the init Process — what makes PID 1 special to the kernel; systemd as the modern occupant of that role
- systemd Units — the unit abstraction and unit-file structure in depth
- systemd Service Units — the service unit,
Type=,Exec*=, restart policy - systemd Targets and the Boot Sequence — targets as runlevel replacements; the boot target chain
- systemd Dependencies and Ordering —
Requires/Wants/After/Beforesemantics (the requirement-vs-ordering split) - systemd and cgroup Integration — slices/scopes/services as the cgroup tree; resource control
- systemd Socket Activation — lazy, parallel startup via held sockets
- The systemd Journal — journald structured logging
- The Init System Comparison — systemd vs sysvinit/OpenRC/runit/s6
- Reaping Orphans and the subreaper — the reaping duty systemd inherits as PID 1, and why containers use tini instead
- systemd-analyze and Boot Profiling —
blame,critical-chain,plot,verify, and how to read their output correctly - cgroups v2 Unified Hierarchy — the kernel substrate systemd’s slice/scope/service tree is built on, and the only hierarchy supported since v258
- systemctl and Unit Management — the D-Bus client that speaks to PID 1
- The systemd Target Ordering sysinit basic multi-user — the boot target chain in detail
- Linux Boot and Init MOC — parent MOC, §6 systemd