systemd Dependencies and Ordering

systemd does not run units in a fixed, scripted sequence the way sysvinit ran numbered rc scripts. Instead it builds a directed graph of units and computes, on demand, a transaction — the set of jobs needed to bring the system to a requested state — then executes those jobs as concurrently as the graph allows. Two completely orthogonal kinds of edge drive that graph, and conflating them is the single most common systemd mistake. Requirement dependencies (Wants=, Requires=, Requisite=, BindsTo=, PartOf=, Conflicts=, Upholds=) decide whether one unit should pull in, fail with, or exclude another. Ordering dependencies (After= and Before= — and only those two) decide when units run relative to each other. The manual is blunt about the separation: “requirement dependencies do not influence the order in which services are started or stopped” (systemd.unit(5)). A Wants= with no After= starts both units simultaneously. You almost always need both kinds of edge, and forgetting the ordering half is why services “randomly” fail to find the resources they depend on.

Mental Model — Two Axes, Not One

The right way to think about systemd dependencies is as a 2D plane, not a single line. One axis answers “if I start A, what else gets started, and does A’s fate depend on theirs?” — the requirement axis. The other answers “of the units that are starting, who must finish first?” — the ordering axis. A unit can sit anywhere on this plane: pulled in but unordered (start together), ordered but not pulled in (only matters if both happen to be in the same transaction), or — the common, correct case — both pulled in and ordered.

The reason the two are separate is that systemd’s whole performance story is parallelism. If Requires= implied After=, every requirement edge would also be a serialization point, and boot would degenerate toward the sequential rc-script model systemd was built to escape. By keeping the axes orthogonal, systemd can pull in a dozen prerequisites and start them all at once, serializing only where an explicit After=/Before= says it genuinely must.

flowchart TB
  subgraph REQ["Requirement axis — WHETHER B is pulled in / shares A's fate"]
    direction TB
    W["Wants= (soft: pull in, ignore failure)"]
    R["Requires= (hard: pull in, fail with it*)"]
    RQ["Requisite= (must ALREADY be up, else fail now)"]
    B["BindsTo= (tied lifecycle: B stops to inactive to A stops)"]
    P["PartOf= (one-way: stop/restart of B to A)"]
    C["Conflicts= (mutual exclusion)"]
    U["Upholds= (continuously restart B while A is up)"]
  end
  subgraph ORD["Ordering axis — WHEN, relative to each other"]
    direction TB
    AF["After= / Before= (the ONLY ordering knobs)"]
  end
  NOTE["A unit picks a point on EACH axis independently.<br/>Wants=foo with no After=foo to start TOGETHER.<br/>*Requires= only propagates failure when an After= edge also exists."]
  REQ -.->|orthogonal| ORD

The two orthogonal axes of systemd dependencies. What it shows: requirement directives (left) and ordering directives (right) are independent dimensions — a unit chooses a requirement relationship and, separately, an ordering relationship. The insight to take: there is no requirement directive that implies ordering and no ordering directive that implies pulling a unit in; the correct, robust recipe for “B must be fully up before A” is the pair Wants=B (or Requires=B) and After=B. The asterisk on Requires= is the subtle part: its failure propagation only kicks in when an After= edge also exists (see below).

Mechanical Walk-through

The transaction: jobs, not a script

When you run systemctl start foo.service — or when the boot reaches default.target — systemd does not “execute foo.” It computes a transaction: a set of jobs (each job is a (unit, type) pair such as foo.service/start). Starting foo whose unit file says Requires=bar.service adds a bar.service/start job to the same transaction. The requirement directives determine which jobs join the transaction and how their outcomes are coupled; the ordering directives determine the partial order in which the transaction’s jobs are dispatched. systemd then runs jobs in that partial order, launching everything not blocked by a pending After= predecessor in parallel.

Requirement directives, precisely

Each requirement directive couples two units’ existence and fate differently. The exact semantics, quoted from systemd.unit(5):

  • Wants= — the soft pull-in. “Units listed in this option will be started if the configuring unit is. However, if the listed units fail to start or cannot be added to the transaction, this has no impact on the validity of the transaction as a whole, and this unit will still be started.” This is the directive you reach for by default: it expresses “I’d like B running too,” and if B can’t start, A carries on anyway. The [Install]-section reverse form (WantedBy=) is what systemctl enable materializes — see below.

  • Requires= — the hard pull-in. “If this unit gets activated, the units listed will be activated as well. 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.” The bolded clause is the trap: Requires=bar without After=bar pulls bar in but does not abort foo if bar fails — because the two started simultaneously, systemd cannot say bar “failed before” foo. The manual itself advises caution: “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.”

  • Requisite=must already be up. “If the units listed here are not started already, they will not be started and the starting of this unit will fail immediately.” Unlike Requires=, it does not pull the dependency in — it asserts a precondition. “Requisite= does not imply an ordering dependency, even if both units are started in the same transaction,” so to use it meaningfully you also order with After=.

  • BindsTo=tied lifecycle. “In addition to the effects of Requires=, which already stops (or restarts) the configuring unit when a listed unit is explicitly stopped (or restarted), it also does so when a listed unit stops unexpectedly.” With After= added, “the unit bound to strictly has to be in active state for this unit to also be in active state.” This is the directive for “if the thing I depend on disappears for any reason — including a crash or a device unplug — tear me down too.” It is heavily used with device units (a service bound to a USB modem’s .device unit dies when the modem is unplugged).

  • PartOf=one-way stop/restart propagation. “Configures dependencies similar to Requires=, but limited to stopping and restarting of units. When systemd stops or restarts the units listed here, the action is propagated to this unit. Note that this is a one-way dependency.” Stopping/restarting the listed unit propagates to the configuring unit, but not the reverse, and starting is never propagated. This is how a control target (e.g. a “my-app” target) cleanly stops or restarts all its member services without affecting startup ordering.

  • Conflicts=mutual exclusion. “If a unit has a Conflicts= requirement on a set of other units, then starting it will stop all of them and starting any of them will stop it.” It “does not imply an ordering dependency, similarly to Wants= and Requires=” — so when a start and a conflicting stop land in the same transaction you generally also add ordering so the stop completes before the start. This is the mechanism behind, e.g., emergency.target conflicting with the normal boot targets.

  • Upholds=continuous restart guarantee. “As long as this unit is up, all units listed in Upholds= are started whenever found to be inactive or failed, and no job is queued for them. While a Wants= dependency on another unit has a one-time effect when this unit is started, a Upholds= dependency on it has a continuous effect, constantly restarting the unit if necessary.” Where Wants= pulls a unit in once, Upholds= keeps it pinned up for as long as the upholding unit lives.

Ordering directives, precisely

After= and Before= are the entire ordering vocabulary. “If unit foo.service contains the setting Before=bar.service and both units are being started, bar.service’s start-up is delayed until foo.service has finished starting up. After= is the inverse of Before=.” Two refinements matter:

  1. Shutdown is the mirror of startup. “When two units with an ordering dependency between them are shut down, the inverse of the start-up order is applied.” So After=B means “start after B” and “stop before B.” And in a mixed transaction where one unit is starting while the other is stopping, “the shutdown is ordered before the start-up.”
  2. Ordering does nothing on its own. If both units are not in the same transaction, an After=/Before= edge is inert. Ordering constrains units that are already both going to run; it never causes a unit to run. That is the whole point of keeping it separate from the requirement axis.

There is one notable carve-out: “Before= dependencies on device units have no effect and are not supported” — you cannot order a unit before a device appearing, because device appearance is driven by the kernel/udev, not by systemd’s scheduler.

The headline rule: Wants= does NOT imply After=

This deserves its own statement because it is the error that bites everyone. From the manual, verbatim: “Note that requirement dependencies do not influence the order in which services are started or stopped. This has to be configured independently with the After= or Before= options. If unit foo.service pulls in unit bar.service as configured with Wants= and no ordering is configured with After= or Before=, then both units will be started simultaneously and without any delay between them if foo.service is activated.”

So Wants=network-online.target alone does not wait for the network — it merely ensures the target is pulled into the transaction, then races your service against it. You need the ordering edge too. This is exactly the worked example below.

Default dependencies — the implicit wiring you forget you have

Every unit, unless it opts out, is born with a set of implicit edges so that ordinary services don’t each have to re-declare “I run after the basic system is initialized and I stop before shutdown.” With DefaultDependencies=yes (the default), “a few default dependencies will implicitly be created for the unit. The actual dependencies created depend on the unit type.” For a typical service these amount, in spirit, to Requires=/After= on sysinit.target, ordering After=basic.target, and Conflicts=/Before=shutdown.target so it is torn down cleanly. For target units there is an extra rule worth knowing: “target units will complement all configured dependencies of type Wants= or Requires= with dependencies of type After=” — i.e. a target (and only a target) does turn its requirement edges into ordering edges, because a target is a synchronization point and that is its whole job. (This is precisely why ordinary services do not get that treatment.)

You opt out with DefaultDependencies=no. This is essential for units that participate in early boot (anything that must run before sysinit.target or basic.target), because the implicit After=basic.target would otherwise create an ordering cycle — your early unit would be ordered both before basic.target (because basic.target needs it) and after it (the default edge). See the cycle-breaking section.

The [Install] section — reverse directives

The directives above live in the [Unit] section and take effect whenever the unit is loaded. The [Install] section is different: it does nothing at runtime and only describes what systemctl enable/disable should do. Its reverse directives let a unit register itself as a dependency of another unit without editing that other unit’s file:

  • WantedBy=multi-user.target — on enable, systemd creates a symlink in multi-user.target.wants/ pointing at this unit. The effect is identical to adding Wants=this.service to multi-user.target, but it lives in your package’s unit file and is reversible by disable. This is the overwhelmingly common way services hook into the boot.
  • RequiredBy= — the hard analogue; creates the symlink in …​.requires/.
  • UpheldBy= — the Upholds= analogue.
  • Also= — enable/disable other units together with this one (e.g. a .socket enabling its paired .service). This is how NetworkManager.service drags in NetworkManager-wait-online.service.
  • Alias= — on enable, create a symlink under an alternative name so the unit can be referred to by that name (the manual’s example: enabling a unit with Alias=ctrl-alt-del.target creates /etc/systemd/system/ctrl-alt-del.target → the unit).

The mental model: WantedBy= is “I want to be wanted by X.” It is a reverse Wants=, materialized as a filesystem symlink at enable-time rather than as an in-memory edge at load-time.

Worked Example — A Service That Needs the Network

Here is the canonical, correct pattern for a service that must not start until the machine actually has working network connectivity (per systemd.io NETWORK_ONLINE and systemd.special(7)):

# /etc/systemd/system/fetch-config.service
[Unit]
Description=Fetch remote configuration at boot
Wants=network-online.target          # (1) pull the target into the transaction
After=network-online.target          # (2) AND wait for it to be reached
# After=nss-lookup.target            # (optional) also wait for DNS resolvers
 
[Service]
Type=oneshot
ExecStart=/usr/local/bin/fetch-config --from https://config.example.com
 
[Install]
WantedBy=multi-user.target           # (3) start at normal boot when enabled

Line-by-line:

  • (1) Wants=network-online.target — without this, network-online.target might never be in the transaction at all (it is opt-in; see below), so there would be nothing for After= to wait on. Wants= is correct rather than Requires= because if the network genuinely never comes up, you usually want your service to at least attempt to run (and fail loudly) rather than be silently cancelled.
  • (2) After=network-online.target — this is the half people omit. Without it, lines (1) and your ExecStart race: systemd pulls the target in and starts your service at the same instant. The After= forces your service to wait until the target is reached, which (because it is an active target) means a wait-service has confirmed the network is up.
  • (3) WantedBy=multi-user.target — the [Install] reverse-Wants= that makes systemctl enable fetch-config.service wire the unit into the normal multi-user boot.

The crucial background fact: network-online.target only does something if a “wait” service is enabled to provide it. Per the systemd.io guidance, “the right ‘wait’ service must be enabled: NetworkManager-wait-online.service if NetworkManager is used …, systemd-networkd-wait-online.service if systemd-networkd is used.” These wait-services are oneshot units that block until the network is configured. NetworkManager-wait-online.service “delays reaching the network-online target until NetworkManager reports startup complete on D-Bus” (NetworkManager docs); systemd-networkd-wait-online.service will “wait for all links it is aware of … to be fully configured or failed, and for at least one link to be online,” where “online means that the link’s operational state is equal or higher than ‘degraded’” (systemd-networkd-wait-online.service(8)). These wait-services are auto-enabled alongside their managers via Also=.

Contrast with network.target, which is not a substitute. network.target is “a passive unit (i.e. pulled in by the provider of the functionality, rather than the consumer) that usually does not delay execution much”; its real job is shutdown ordering — After=network.target ensures your service stops before the network stack goes down. The systemd.io doc is explicit that “at start-up there’s no guarantee that hardware-based devices have shown up by the time this target is reached, or even acquired complete IP configuration. For that purpose use network-online.target.” Using After=network.target when you meant network-online.target is itself a frequent bug.

Failure Modes

The classic: Wants= without After=

Symptom: a service “works when I start it by hand but fails at boot,” or fails intermittently — perhaps one boot in four. Cause: the service Wants= something but does not order itself After= it, so it races the dependency and sometimes wins the race (i.e. starts before the dependency is ready). Diagnose with systemctl list-dependencies foo.service (shows the requirement graph but not ordering) and systemd-analyze critical-chain foo.service (shows the time-ordered chain — if the dependency isn’t an ancestor in the critical chain, you forgot the After=). Fix: add the After=.

Ordering cycles — and why they brick boot non-deterministically

If the ordering edges (including the implicit default ones) form a loop, systemd cannot produce a valid execution order. Rather than refuse to boot, systemd breaks the cycle by deleting one job from it, logging lines such as:

systemd[1]: Found ordering cycle on sysinit.target/start
systemd[1]: Found dependency on local-fs.target/start
systemd[1]: …
systemd[1]: sysinit.target: Job systemd-tmpfiles-setup.service/start deleted to break ordering cycle starting with sysinit.target/start

(Arch bug FS#42633, peng.fyi cycle write-up). The dangerous part is that the choice of which job to delete is effectively arbitrary — “breaking a cycle can be done by removing any edge in the loop.” On one boot systemd may delete a harmless job and the system comes up fine; on the next it deletes a critical one (e.g. systemd-tmpfiles-setup.service, which creates /var/lib/… directories) and a downstream service then fails because its expected paths don’t exist. The peng.fyi post documents a real incident where only ~28% of boots failed precisely because of this non-determinism. The usual root cause: a unit that orders itself Before= something early (like local-fs.target or basic.target) while also inheriting the default After=basic.target edge. The fix is almost always DefaultDependencies=no on the early unit so the implicit edges that close the loop are never created (Red Hat solution 3032831). Diagnose ahead of time with systemd-analyze verify /path/to/foo.service, which reports cycles statically.

Requires= that doesn’t propagate failure

Symptom: “I used Requires= so my service should have been cancelled when its dependency failed, but it started anyway.” Cause: no After= edge, so systemd couldn’t establish that the dependency failed before the configuring unit started. Fix: pair Requires= with After= — or, better, reconsider whether you wanted Requires= at all (the manual recommends Wants= for robustness).

BindsTo= surprises on transient device loss

Symptom: a service bound to a device tears down the instant the device blips out. That is BindsTo= working as designed (“stops … when a listed unit stops unexpectedly”). If you wanted it to survive a brief disappearance, Wants=/Requires= is the gentler choice.

Alternatives and When to Choose Them

The decision among requirement directives is essentially a robustness-vs-strictness dial:

  • Reach for Wants= by default — soft, failure-tolerant, the manual’s own recommendation for robustness.
  • Use Requires= only when the dependency is genuinely mandatory and you pair it with After= so failure actually propagates.
  • Use Requisite= when you want to assert a precondition is already met without starting it (e.g. a unit that should only run inside an already-active environment).
  • Use BindsTo= for lifecycle-glued pairs — service-to-device, or a worker that is meaningless without its supervisor.
  • Use PartOf= to build a controllable group (a target whose stop/restart cascades to members) without imposing startup ordering.
  • Use Upholds= when you need a unit kept perpetually up (a stronger guarantee than a Restart=on-failure inside the unit itself, because it survives even a clean exit).
  • Use Conflicts= for genuinely mutually-exclusive states (rescue vs normal boot).

Against the older world: this entire graph model replaces sysvinit’s fixed, numerically-ordered rc scripts. sysvinit had exactly one axis — sequence number — which conflated “when” with “whether” and serialized everything. systemd’s two-axis model is strictly more expressive and is what enables parallel boot; the cost is the conceptual subtlety this whole note is about.

Production Notes

In real fleets the dependency graph is dominated not by hand-written edges but by implicit/default ones, which is why systemd-analyze critical-chain (the ordered view) and systemctl list-dependencies (the requirement view) are both indispensable — they show different axes. Boot-slowness investigations almost always end at an After=network-online.target (the wait-service blocking ~15-120s) or a poorly-placed ordering edge serializing things that could be parallel; see systemd-analyze and Boot Profiling. The cycle-breaking non-determinism is a genuine production hazard: because it manifests as flaky, low-percentage boot failures, it is easy to misattribute to hardware or to “a bad boot.” The defensive habits are (1) put DefaultDependencies=no on anything touching early boot, (2) run systemd-analyze verify in CI on every shipped unit file, and (3) never assume a requirement edge gives you ordering. The two-axis discipline, internalized, is most of what separates a robust unit file from a flaky one.

See Also