Level 2

systemd boot targets and init system deep dive

Maximilian B. 9 min read 15 views

systemd is the init system on every major Linux distribution you will encounter in 2026: Debian 13.3, Ubuntu 24.04.3 LTS, Ubuntu 25.10, Fedora 43, RHEL 10.1, and RHEL 9.7. If you already know how to run systemctl start and systemctl enable, this article goes deeper into how systemd boot targets work, what dependency directives actually mean, how to read and write unit files, and how to measure and optimize boot times. For the basics of service management with systemctl, see our systemctl practical playbook.

systemd Architecture: Units, Targets, and the Dependency Graph

systemd boot targets and init system deep dive visual summary diagram
Visual summary of the key concepts in this guide.

systemd organizes everything as units. A unit is a configuration file that describes a resource: a service, a mount point, a device, a socket, a timer, or a target. Targets are the key concept for understanding boot flow. They group other units together and replace the old SysV runlevel model, providing named synchronization points that the boot process passes through in sequence.

systemd boot target hierarchy diagram showing the sequential chain from kernel PID 1 through sysinit.target, basic.target, network-pre.target, network.target, to multi-user.target (server default) and graphical.target (desktop). Includes unit type reference (.service, .target, .socket, .timer, .mount, .path), recovery targets (rescue and emergency), unit file precedence locations, and the four dependency directives (Wants, Requires, After, BindsTo).

The main unit types you will work with:

Unit type Suffix Purpose
Service .service Daemons and one-shot tasks
Target .target Grouping unit (like a synchronization point)
Mount .mount Filesystem mount points (auto-generated from fstab)
Socket .socket Socket-activated services (starts service on first connection)
Timer .timer Scheduled execution (replaces cron for many use cases)
Path .path Triggers a unit when a filesystem path changes

All units form a dependency graph. systemd calculates the graph at boot and starts units in parallel wherever dependencies allow it. This parallelism is what makes systemd boots faster than sequential SysV init scripts. Understanding this graph is essential for troubleshooting startup ordering issues and writing correct unit files.

Boot Target Hierarchy: From Kernel Handoff to Login Prompt

When the kernel hands control to systemd (PID 1), systemd activates the default target and pulls in its entire dependency chain. The GRUB2 bootloader loads the kernel and initramfs -- as described in our UEFI and GRUB2 configuration guide -- then systemd takes over. The standard boot target hierarchy looks like this:

# Simplified dependency chain for multi-user.target boot:

sysinit.target          # Early system initialization (udev, tmpfiles, sysctl)
  |
basic.target            # Sockets, timers, paths, slices
  |
network-pre.target
  |
network.target          # Network interfaces up
  |
multi-user.target       # Full multi-user system (no GUI)
  |
graphical.target        # Desktop environment (pulls in display-manager.service)

In production servers, the default target is almost always multi-user.target. Desktop and workstation installs use graphical.target. Two special targets exist for recovery:

rescue.target
Mounts all filesystems, starts a single-user shell. Network is not started. Roughly equivalent to old SysV runlevel 1. Use this when you need to fix service configurations, user accounts, or daemon problems while having full filesystem access.
emergency.target
Mounts only the root filesystem (often read-only), starts an emergency shell. Almost nothing else runs. Use this when rescue.target itself fails -- typically because of a broken /etc/fstab entry or corrupt filesystem that prevents normal mounting.

Viewing and changing the default boot target

# Check current default target
systemctl get-default
# multi-user.target

# Change default target to graphical
sudo systemctl set-default graphical.target

# Change default target to multi-user (no GUI)
sudo systemctl set-default multi-user.target

Switching targets at runtime with systemctl isolate

The isolate command switches to a target immediately, stopping all units that the new target does not need. This is how you change the effective "runlevel" on a running system without rebooting:

# Switch from graphical to multi-user (stops display manager)
sudo systemctl isolate multi-user.target

# Switch to rescue mode (drops most services)
sudo systemctl isolate rescue.target

# Switch to emergency mode
sudo systemctl isolate emergency.target

Not every target supports isolate. A target must have AllowIsolate=yes in its unit file. The standard boot targets (multi-user, graphical, rescue, emergency) all have this set. Custom targets you create must explicitly include this directive if you want to isolate to them.

Unit File Anatomy: Writing and Understanding systemd Service Files

Understanding how to write correct systemd unit files is a core skill for Linux administration. Unit files live in three locations, in order of precedence:

  1. /etc/systemd/system/ - administrator overrides (highest priority)
  2. /run/systemd/system/ - runtime-generated units
  3. /usr/lib/systemd/system/ - vendor/package defaults (lowest priority)

A typical service unit file has three sections:

# /etc/systemd/system/myapp.service

[Unit]
Description=My Application Server
Documentation=https://docs.example.com/myapp
After=network.target postgresql.service
Requires=postgresql.service
Wants=redis.service

[Service]
Type=notify
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
ExecStartPre=/opt/myapp/bin/check-config
ExecStart=/opt/myapp/bin/server --config /etc/myapp/config.yml
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5
TimeoutStartSec=30
TimeoutStopSec=30
Environment=NODE_ENV=production
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target

Dependency Directives: Wants, Requires, After, and Before

These are the directives that administrators confuse most often. Getting them wrong leads to services that start before their dependencies are ready, or services that fail silently because a soft dependency was not pulled in:

Side-by-side comparison of the four systemd dependency directives: After= (ordering only, unit continues if X fails), Wants= (soft dependency, continues if X fails), Requires= (hard dependency, both fail together), and BindsTo= (coupled lifecycle, auto-stops if X deactivates at runtime). Each row shows a behavior diagram and the common mistake of using network.target instead of network-online.target.
After=X
Ordering only. "Start me after X starts." Does not pull X into the transaction. If X is not otherwise requested, After=X has no effect by itself.
Requires=X
Hard dependency. If X fails to start, this unit also fails. If X is stopped or restarted, this unit is stopped too. Use this for services that genuinely cannot function without X.
Wants=X
Soft dependency. Tries to start X, but this unit continues even if X fails. This is the recommended default for most dependencies where your service prefers X but can degrade gracefully without it.
BindsTo=X
Like Requires but also stops this unit if X is deactivated at any time, not just during startup. Use this for units that are tightly coupled to a specific device or mount point.

A common mistake: using After=network.target alone. This orders your service after network.target, but does not guarantee the network is actually ready. If your service needs working DNS or a routable IP, use:

[Unit]
After=network-online.target
Wants=network-online.target

The network-online.target waits until at least one interface has a configured address. This distinction matters on servers with slow DHCP or bonded interfaces. For deeper understanding of how kernel modules interact with hardware detection during early boot, see our kernel modules and hardware detection overview.

The [Install] section and WantedBy

The [Install] section is only read by systemctl enable and systemctl disable. When you enable a service, systemd creates a symlink in the target's .wants directory:

# Enabling a service
sudo systemctl enable myapp.service
# Created symlink /etc/systemd/system/multi-user.target.wants/myapp.service
#   -> /etc/systemd/system/myapp.service

# Disabling removes the symlink
sudo systemctl disable myapp.service

If you modify a unit file, reload the systemd daemon before the change takes effect:

sudo systemctl daemon-reload

Boot Time Analysis and Optimization with systemd-analyze

systemd includes built-in boot profiling tools. On a production server, slow boots waste time during maintenance windows and delay failover recovery. Identifying and eliminating boot bottlenecks can shave minutes off your recovery time during outages.

# Overall boot time breakdown
systemd-analyze
# Startup finished in 2.345s (firmware) + 1.012s (loader) + 1.889s (kernel)
#                  + 8.234s (userspace) = 13.480s total

# List units by startup time (slowest first)
systemd-analyze blame
# 4.201s NetworkManager-wait-online.service
# 1.892s dracut-initqueue.service
# 1.345s firewalld.service
# 0.987s systemd-udev-settle.service
# ...

# Show the critical chain (the longest dependency path)
systemd-analyze critical-chain
# graphical.target @8.234s
#   multi-user.target @8.233s
#     postgresql.service @6.012s +2.221s
#       network-online.target @5.998s
#         NetworkManager-wait-online.service @1.789s +4.201s
#           ...

# Generate an SVG boot chart
systemd-analyze plot > /tmp/boot-chart.svg

Common boot bottlenecks and how to fix them

After running systemd-analyze blame, you will often find the same culprits at the top of the list. Here are the most common bottlenecks and their solutions:

NetworkManager-wait-online.service
Often the slowest unit, sometimes taking 10-30 seconds on systems with multiple interfaces or slow DHCP servers. If the service waiting for network does not actually need it at boot, remove the Wants=network-online.target dependency from that service. On servers with static IPs, this unit typically completes quickly -- if it does not, check for unconfigured interfaces that NetworkManager is waiting on.
systemd-udev-settle.service
Waits for all udev events to finish processing. Usually only needed for specific hardware setups. Mask it if nothing depends on it: sudo systemctl mask systemd-udev-settle.service. Before masking, verify with systemctl list-dependencies --reverse systemd-udev-settle.service that no critical units require it.
Slow storage initialization
Shows up in dracut/initramfs time. Check for unnecessary LVM scans, broken multipath configurations, or storage controllers with long initialization times. On virtual machines, ensure the virtio storage driver is included in the initramfs.

Using drop-in overrides to customize units safely

Instead of editing vendor unit files directly (which get overwritten on package updates), use drop-in overrides. This is the systemd-native way to customize service behavior:

# Create a drop-in directory and override file
sudo systemctl edit myapp.service
# This creates /etc/systemd/system/myapp.service.d/override.conf
# and opens it in your editor

# Example override: increase file descriptor limit and add environment variable
[Service]
LimitNOFILE=131072
Environment=DEBUG=true

Verify the effective configuration after overrides:

systemctl show myapp.service --property=LimitNOFILE,Environment

# To see the full merged unit file:
systemctl cat myapp.service

systemd Boot Targets and Service Management Quick Reference

Task Command
Check default boot target systemctl get-default
Set default boot target sudo systemctl set-default multi-user.target
Switch target at runtime sudo systemctl isolate rescue.target
List all loaded targets systemctl list-units --type=target
Show failed units systemctl --failed
Analyze boot time systemd-analyze
Show slowest units systemd-analyze blame
Show critical boot path systemd-analyze critical-chain
Generate boot chart SVG systemd-analyze plot > boot.svg
Edit unit with drop-in override sudo systemctl edit myapp.service
Reload after unit file changes sudo systemctl daemon-reload
Show effective unit config systemctl cat myapp.service
List all dependencies of a unit systemctl list-dependencies multi-user.target
Mask a unit (prevent starting) sudo systemctl mask unit.service

Summary

systemd manages the entire Linux boot sequence through a dependency graph of units and targets. The default target (usually multi-user.target on servers) determines what gets started. Understanding the dependency directives -- After for ordering, Requires for hard dependencies, Wants for soft ones -- prevents the most common service startup failures. Use systemd-analyze blame and critical-chain to identify boot bottlenecks. Write drop-in overrides instead of editing vendor unit files. And when things go wrong, rescue.target and emergency.target give you the minimal environments needed to diagnose and repair without a live USB. For detailed procedures on using those recovery targets to fix kernel failures and filesystem problems, continue to our system recovery and emergency targets guide.

Share this article
X / Twitter LinkedIn Reddit