Targets and Boot
A systemd target is a unit that groups other units and acts as a synchronization point during boot. It carries no process of its own — multi-user.target does not run anything, it just declares "the system has reached the state where all the services a server needs are up." Targets replaced SysV runlevels, but they are not numbered slots you flip between; they are named nodes in a dependency graph, and a target can pull in any number of other targets.
Boot is that graph being resolved. systemd starts at a single default.target, walks every Wants and Requires edge to find everything it must activate, and starts those units in parallel, constrained only by the explicit Before/After ordering you declare. The operational consequence: when a server hangs on boot or comes up missing a service, you are debugging a graph, not a linear script — and the tools systemd gives you (systemctl list-dependencies, systemd-analyze) exist to make that graph legible.
Targets as Unit Groups
A .target unit exists to be depended on. It bundles a set of services and other targets under one name so you can refer to "the multi-user state" rather than enumerating thirty services. The two you meet first are multi-user.target — full system, networking, all daemons, no graphical login — and graphical.target, which pulls in multi-user.target and then adds the display manager on top. On a server you want the former; the latter drags in X or Wayland you have no reason to run.
Membership is expressed two ways, and the distinction matters. A unit listed under a target's Wants= is started alongside it but its failure does not fail the target; a unit under Requires= is mandatory, and if it fails the target is considered failed and its dependents are torn down. In practice most service-to-target links are Wants, created automatically when you systemctl enable a service — enabling drops a symlink into the target's .wants/ directory.
# what does multi-user.target actually pull in? systemctl list-dependencies multi-user.target # the enable symlink that makes a service part of the target systemctl enable nginx.service # -> /etc/systemd/system/multi-user.target.wants/nginx.service
The Default Target
default.target is the unit systemd activates at the end of boot, and it is nothing more than a symlink. On a Debian or Ubuntu server it points at multi-user.target; on a desktop install it points at graphical.target. The old runlevels map onto these targets through compatibility symlinks — runlevel3.target is multi-user.target, runlevel5.target is graphical.target — but the mapping is a convenience, not a one-to-one identity, because targets can be composed in ways runlevels never could.
Change the default through systemd, never by editing the symlink yourself. systemctl set-default rewrites /etc/systemd/system/default.target atomically and validates that the target exists; a hand-edited symlink that points at a typo or a nonexistent unit drops the machine to emergency mode on the next boot with no obvious cause.
# read and change the boot default systemctl get-default multi-user.target systemctl set-default multi-user.target # Removed /etc/systemd/system/default.target. # Created symlink ... -> /usr/lib/systemd/system/multi-user.target.
Boot Sequence and Ordering
Below the target you choose sits a fixed scaffold every boot climbs through. sysinit.target gathers early setup — mounting filesystems, setting up swap, applying sysctl, starting udev. basic.target follows once sockets, timers, and paths are ready. Only after basic.target is reached do the bulk of system services start, and finally the chosen default.target caps the chain. Each layer is ordered After the one below it, which is how parallel startup stays correct without a hand-written sequence.
The point people miss is that Wants/Requires and Before/After are independent. Dependency settings decide whether a unit is pulled in; ordering settings decide when it runs relative to others. A service can Want the network and still race ahead of it unless it also declares After=network-online.target and that target is wanted. Forgetting the ordering half is the classic reason a daemon starts before its database socket exists and crashes on the first boot after a reboot.
# a service that must wait for the network to be usable [Unit] Wants=network-online.target After=network-online.target [Service] ExecStart=/usr/bin/myapp --listen 0.0.0.0:8080
Isolating and Rescue
systemctl isolate switches the running system to a different target now, starting everything that target wants and stopping everything outside it. rescue.target brings the machine to a single-user state with a root shell and local filesystems mounted but no general services or networking; emergency.target goes further, starting an emergency shell on the main console with no other services or mounts pulled in — the root filesystem stays in whatever state it was reached in (read-only if you booted with ro, since the target does no remounting of its own). Both are recovery states, and isolate-ing into them on a remote box without console access cuts your own connection — there is no networking and no sshd left running.
The safer entry point for recovery is the boot loader. Append systemd.unit=rescue.target (or emergency.target) to the kernel command line in GRUB, and the machine boots straight into that state from power-on rather than transitioning a live system. This is the reliable way to fix a box that fails to reach multi-user.target — a broken fstab entry, a service that hangs the boot — because you get a shell before the failing units are ever started.
# switch a local machine to rescue mode interactively systemctl isolate rescue.target # or boot into it from GRUB by editing the linux line: linux /vmlinuz ... ro systemd.unit=rescue.target
Boot Performance
systemd-analyze tells you where boot time went. The bare command prints the split between the boot phases — kernel, initrd, and userspace, plus firmware and loader on UEFI systems; systemd-analyze blame lists every unit sorted by how long it took to initialize. Blame is the first place to look when a server takes ninety seconds to come up, but read it carefully — a unit at the top of the list is not necessarily on the boot's critical path, it may have been running in parallel while nothing waited on it.
systemd-analyze critical-chain is the answer to that. It walks the ordering graph and shows the actual longest dependency path to a target, with the time each step contributed and the timestamp it finished. That is what you optimize: a unit on the critical chain delays everything after it, while a slow unit off the chain costs nothing. Common offenders are NetworkManager-wait-online or systemd-networkd-wait-online blocking on an interface that will never get an address, and a service whose After= chain is longer than it needs to be.
# overall split, then per-unit, then the real critical path systemd-analyze Startup finished in 3.1s (kernel) + 8.7s (userspace) = 11.8s systemd-analyze blame systemd-analyze critical-chain multi-user.target
- Treating targets as runlevels one-to-one — expecting exactly seven numbered states when targets are composable graph nodes, so
runlevel3.targetis a compatibility alias formulti-user.target, not a distinct level. - Editing the
default.targetsymlink by hand. Point it at a typo or a unit that does not exist and the next boot drops to emergency mode with no obvious cause;systemctl set-defaultvalidates the target first. systemctl isolate-ingrescue.targetoremergency.targeton a remote server — it stops networking and sshd, cutting your own session, and you need console access to get back in.- Confusing
Wants/RequireswithBefore/After. Declaring a dependency on the network without also declaringAfter=network-online.targetlets the service race ahead and crash before the socket exists. - Reading
systemd-analyze blameas the boot critical path. A unit at the top may have run in parallel off the critical chain, so "fixing" it changes nothing —critical-chainshows what actually delays boot. - Leaving
graphical.targetas the default on a headless server, dragging in a display manager and X libraries that consume memory and widen the attack surface for no benefit.
- Change the boot default with
systemctl set-default, never by hand-editing the symlink — it validates the target and writes the link atomically. - Keep
multi-user.targetas the default on every server; reservegraphical.targetfor machines that genuinely need a desktop. - Test a target switch with
systemctl isolateon a console-reachable machine before committing it withset-default, so a mistake is one reboot away from recovery. - Debug slow boots with
systemd-analyze critical-chain, notblamealone — optimize only units that sit on the real dependency path to your target. - Recover an unbootable machine by appending
systemd.unit=rescue.targetto the kernel line in GRUB, so you get a shell before the failing units run. - Pair every
Wants=network-online.targetwithAfter=network-online.targetin service units so ordering matches the dependency and the service never races the network.
/etc/inittab, rc.d scripts) that targets replaced; sequential, not a dependency graphUpstart — Ubuntu's pre-systemd event-driven init, where job state was triggered by emitted events rather than a resolved target graphWindows services.msc — per-service startup types (Automatic, Manual, Disabled) with dependency lists, the closest mainstream contrast to enabling a service into a targetKnowledge Check
A service declares Wants=network-online.target but no ordering directive, and it crashes on boot because the network is not up yet. What is missing?
After=network-online.target—Wantspulls the target in but does not order the service after it, so the service can still start first- A
Requires=directive used in place ofWants=, because onlyRequiresactually makes the unit wait for the named dependency to finish starting first - An explicit
enableofnetwork-online.target, because targets do not ever activate unless they are enabled by hand - A higher numbered runlevel set in the unit's
[Install]section in order to delay when it starts
You need to recover a server that hangs before reaching multi-user.target. Which approach is safest?
- Append
systemd.unit=rescue.targetto the kernel line in GRUB so the machine boots into a shell before the failing units start - SSH into the box and run
systemctl isolate emergency.targetto drop the running services and then calmly inspect the broken system - Edit the
default.targetsymlink to point atrescue.targetfrom another machine's recovery mount - Boot normally and then run
systemd-analyze blameto find the one unit that is hanging boot
Why is systemd-analyze critical-chain more useful than blame for cutting boot time?
- It shows the longest dependency path to the target, so the units it lists actually delay boot, whereas a slow unit in
blamemay have run in parallel off the critical path blameonly measures the kernel-side time spent before the initramfs hands off, whilecritical-chaininstead measures all of the userspace startup time that comes after itcritical-chainsorts the units it prints by peak memory usage rather than by their measured start durationblamesilently excludes every failed unit from its output, so it always undercounts the real total boot time
What is the relationship between runlevel3.target and multi-user.target on a modern systemd machine?
runlevel3.targetis a compatibility alias (symlink) formulti-user.target, provided so old runlevel habits keep working- They are two independent targets that must both be reached separately, in strict sequence, during a normal boot
runlevel3.targetis the kernel-level boot state, whilemulti-user.targetis its entirely separate user-space equivalentmulti-user.targetpulls inrunlevel3.targetthe waygraphical.targetpulls inmulti-user.target
You got correct