open atlas
↑ Back to track
Linux, the operating system LIN · 03 · 03

Dependencies and ordering

Wants/Requires declare what must exist; After/Before declare what must come first. They are orthogonal — you can have ordering without dependency and dependency without ordering. Most bugs come from confusing the two.

LIN Middle ◷ 22 min
Level
FoundationsJuniorMiddleSenior

Your API service starts at boot, connects to Postgres, and immediately crashes — because Postgres is still initialising. You add After=postgresql.service and it still crashes, because After= alone does not make systemd wait for Postgres to be ready, only for its unit to have been activated. You add Requires=postgresql.service and now when Postgres crashes at 2am, your API is silently killed with it — which was not what you wanted either. The dependency system is one paragraph in the man page and a week of production incidents if you get it wrong.

Goal

After this lesson you can explain the difference between dependency directives (Wants, Requires) and ordering directives (After, Before), describe what happens when a required unit fails, understand how WantedBy wires a service into a target at enable time, and diagnose the common race condition from using After= without a dependency directive.

1

Dependency directives (Wants, Requires) answer “does this unit need to be active?” Ordering directives (After, Before) answer “which unit starts first?” They are completely orthogonal.

You can have ordering without dependency: After=network.target means “start after the network target has been reached” but if the network target fails, your service still tries to start. You can have dependency without ordering: Requires=postgresql.service means “if postgres is not active, fail” but without After=postgres.service systemd may try to start both at the same time (parallel activation).

In practice you almost always want both:

[Unit]
# Dependency: if postgres is not running, this service cannot function
Requires=postgresql.service
# Ordering: start this service AFTER postgres has activated
After=postgresql.service

Omitting After= with Requires= is the race condition that bites most people: both services start in parallel, your service connects before postgres is listening, and it fails — even though technically the dependency is declared.

2

Wants= vs Requires= is about failure tolerance. This is the most important distinction in the dependency system:

DirectiveIf the dependency fails at startupIf the dependency stops later
Wants=This unit starts anywayThis unit keeps running
Requires=This unit fails tooThis unit is stopped
BindsTo=This unit fails tooThis unit is stopped immediately

Wants= is the right choice for optional dependencies — a metrics exporter that works better with Redis but degrades gracefully without it. Requires= is for hard dependencies — a service that cannot function at all without the database. BindsTo= is for tightly coupled pairs like a service and its companion network namespace unit.

[Unit]
# Soft dependency: try to start redis, but proceed even if it fails
Wants=redis.service
After=redis.service

# Hard dependency: if postgres fails, stop this service too
Requires=postgresql.service
After=postgresql.service
3

After= and Before= control ordering within a parallel activation. systemd’s default is to start units in parallel — the dependency graph fans out and everything whose deps are satisfied starts simultaneously. After= inserts a sequencing constraint:

[Unit]
# Do not start until network-online.target is reached
After=network-online.target
# Ensure this service stops before the network goes down (on shutdown)
Before=network.target

network.target vs network-online.target is a common pitfall: network.target is reached when the network interfaces are up, which happens almost immediately. network-online.target is reached when the network is routable — DNS resolves, default route exists. A service that makes outbound connections needs After=network-online.target, not After=network.target.

# See what network-online.target actually depends on
systemctl list-dependencies network-online.target
4

WantedBy= in [Install] is what systemctl enable acts on. It does not create a dependency from your service to the target — it creates one in the other direction: when systemctl enable myapp.service is run, it creates a symlink at /etc/systemd/system/multi-user.target.wants/myapp.service. When systemd activates multi-user.target at boot, it reads that wants/ directory and starts everything in it.

[Install]
WantedBy=multi-user.target

This is why the [Install] section has no effect until you run systemctl enable. The section describes how to install the unit, not a live dependency. multi-user.target is the right choice for any service that should run in a normal (non-graphical, networked) system. For services that only make sense with a GUI, use graphical.target.

# See what multi-user.target will start (after enabling your service)
ls /etc/systemd/system/multi-user.target.wants/
5

How a target pulls in its dependencies — and why this matters for your own targets. A target is just a named synchronisation point. When systemd activates multi-user.target, it:

  1. Reads its Wants= and Requires= directives (static, in the unit file)
  2. Reads its wants/ and requires/ drop-in directories (dynamic, populated by systemctl enable)
  3. Starts all units found by steps 1 and 2, honouring their After= constraints
# See the full dependency tree for multi-user.target
systemctl list-dependencies multi-user.target

# See only the units directly wanted by the target
systemctl list-dependencies --plain multi-user.target | head -20

Understanding this chain — target → wants dir → your service — is what makes systemctl enable feel less like magic. Enable just drops a symlink into a directory that systemd already reads.

Worked example

Debug a service that races with its database at boot.

Symptom: myapi.service fails on boot with “connection refused” to PostgreSQL, but starts fine if you run systemctl restart myapi thirty seconds later.

# Check the unit's dependency declarations
systemctl cat myapi.service
# [Unit]
# After=postgresql.service      ← ordering only, no dependency
# (no Requires= line)

# Check what postgresql.service looks like at the time of failure
journalctl -b -u postgresql.service --since "boot" | head -20
# postgresql.service: started (Type=notify), but not yet accepting connections

The race: After=postgresql.service means systemd waits for the postgresql unit to transition to active state. For Type=notify, active is reached when the process sends READY=1 — which postgres sends after it has forked but before it is accepting TCP connections. The API tries to connect in that gap.

Fix:

[Unit]
Description=My API
Requires=postgresql.service
After=postgresql.service

Then add a connection-retry loop in the application itself — a service should be resilient to its dependencies being briefly unavailable even with correct unit ordering, because ordering guarantees sequence but not readiness. Alternatively, use ExecStartPre to poll:

[Service]
ExecStartPre=/bin/sh -c 'until pg_isready -h localhost; do sleep 1; done'
ExecStart=/opt/myapi/server
Common mistake

A common overreach: writing Requires=network.target thinking it will wait for the network to be fully up. network.target is a passive target that is “reached” the moment the network manager starts — not when interfaces are configured. Use After=network-online.target (and Wants=network-online.target) for services that need an actual routable network. On Ubuntu, systemd-networkd-wait-online.service or NetworkManager-wait-online.service is what actually populates network-online.target.

Why this works

SysV init had no equivalent to this system — script numbering (S20postgresql, S25myapp) was the only ordering mechanism, and there was no concept of “if postgres fails, stop myapp”. The ordering was global and sequential; there was no parallelism and no failure propagation. systemd’s dependency graph enables both faster boots (parallel) and safer failure handling (propagated stops) — but it requires you to declare your intent explicitly.

Check yourself
Quiz

Your unit file has `After=postgresql.service` but no `Requires=` or `Wants=`. PostgreSQL fails to start at boot. What happens to your service?

Recap

Dependency directives (Wants=, Requires=) declare whether a unit must be active. Ordering directives (After=, Before=) declare which starts first. They are orthogonal — using one without the other causes the two classic bugs: a race condition (ordering without dependency) or a cascading stop (hard dependency without understanding that Requires= propagates failure). Wants= is for optional dependencies; Requires= for hard ones. After=network-online.target (not network.target) is what services with outbound connections actually need. WantedBy=multi-user.target in [Install] is what systemctl enable acts on — it drops a symlink that the target reads at boot.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 4 done

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

Apply this

Put this lesson to work on a real build.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources1
expand
  1. 01

Trademarks belong to their respective owners. Editorial reference only.