Reported symptoms
api.servicefails to start on roughly one boot in four across a fleet of 40 hosts. On the other three it is fine.- After a failed boot,
sudo systemctl start apiworks immediately and the service runs for weeks. - The application team points out that the same binary, same config, runs on every host — including the ones that just failed, once restarted.
- Six months ago someone added
After=network-online.targetto the unit. The failures continued. - Someone else added
ExecStartPre=/bin/sleep 10. The failure rate fell to about one boot in twenty, which read as progress. - Last month a BIOS update cut POST time by 18 seconds and the failure rate went back up to one in four.
Evidence provided
$ systemctl status api.service
* api.service - Platform API
Loaded: loaded (/etc/systemd/system/api.service; enabled)
Active: failed (Result: exit-code) since Tue 2026-08-11 09:01:12 UTC
Process: 918 ExecStart=/usr/local/bin/api --config /etc/api/api.yaml (code=exited, status=1/FAILURE)
$ sudo journalctl -u api -b --no-pager | tail -3
Aug 11 09:01:12 host07 api[918]: listening on 192.0.2.50:8443
Aug 11 09:01:12 host07 api[918]: fatal: bind 192.0.2.50:8443: cannot assign requested address
Aug 11 09:01:12 host07 systemd[1]: api.service: Failed with result 'exit-code'.
$ systemctl show -p After api.service
After=systemd-journald.socket sysinit.target network-online.target basic.target system.slice
$ systemctl show -p Wants -p Requires api.service
Requires=sysinit.target
Wants=
$ systemctl is-active network-online.target
inactive
$ systemctl is-enabled systemd-networkd-wait-online.service
disabled
$ systemd-analyze critical-chain api.service
The time when unit became active or started is printed after the "@" character.
api.service @9.412s
`-basic.target @9.401s
`-sockets.target @9.399s
`-dbus.socket @9.395s
`-sysinit.target @9.388s
$ sudo journalctl -b -u systemd-networkd --no-pager | tail -2
Aug 11 09:01:12 host07 systemd-networkd[701]: eth0: DHCPv4 address 192.0.2.50/24 via 192.0.2.1
Aug 11 09:01:13 host07 systemd-networkd[701]: eth0: Gained carrier
Work the evidence before reading on
Three of those commands contradict the unit file, and one of them is the whole answer:
systemctl show -p Afterdoes listnetwork-online.target. So the ordering line is present and systemd parsed it.systemctl is-active network-online.targetsaysinactive— on a host that is up, networked and serving traffic.systemd-analyze critical-chaindoes not mention any network unit at all.
Before continuing, answer: what does After=X mean when X never
starts? That single question is the difference between a fix that works
and the fix that has been in this unit file for six months.
Root cause
1. network.target does not mean the network is up
This is the most consistently misread target in systemd. network.target
is a synchronisation point meaning network management has been started.
It is reached when systemd-networkd or NetworkManager has been
launched — not when a link has carrier, not when DHCP has replied, and
certainly not when an address is assigned.
network-online.target is the one that means “at least one interface is
configured”, and it means that only because a wait-online service sits
in front of it and blocks until the network manager reports an address.
2. After= without Wants= orders against nothing
This is the defect that survived six months of investigation.
After= is purely an ordering statement. It says: if both units are
being started in the same transaction, start the other one first. It does
not cause the other unit to be started.
network-online.target is not pulled in by anything by default. If no
unit Wants= it, it is never part of the boot transaction, so:
- systemd sees
After=network-online.target - the target is not in the transaction
- the ordering constraint is vacuously satisfied
api.servicestarts immediately
The unit file looks correct. systemctl show -p After confirms the
line took effect. And it accomplishes nothing.
# What was written - a no-op
[Unit]
After=network-online.target
# What was needed - both halves
[Unit]
Wants=network-online.target
After=network-online.target
3. The wait-online service was disabled
Even with Wants=, the target only delays anything if something makes it
wait. On this host systemd-networkd-wait-online.service is disabled, so
network-online.target would activate instantly and the ordering would
still be worthless. The pair has to be:
- the correct wait-online service for the network manager actually in use, enabled, and
- a unit that
Wants=and isAfter=the target.
Mismatching those is common on hosts that migrated between
NetworkManager and systemd-networkd — the old wait-online service
stays enabled, reports success for a stack that is no longer managing the
interface, and the target goes active while the address is still absent.
4. Why it is intermittent, and why the firmware update made it worse
The race is between two independent timelines:
- systemd’s start-up path to
api.service: sysinit, sockets, basic, then the unit. Affected by journal flush size, udev settle, disk speed, and every other unit in the transaction. - DHCP completing on eth0: affected by switch port forwarding delay, spanning-tree convergence, and DHCP server latency.
Neither is bounded. On most boots the second finishes first. On some it does not.
The BIOS update removed 18 seconds of POST, which does not change either timeline directly — but it moved the whole boot earlier relative to the switch port coming up, so the network side got relatively slower and the race tilted.
Resolution
- Establish which network manager actually owns the interface.
networkctl status eth0for systemd-networkd,nmcli device show eth0for NetworkManager. Enabling the wrong wait-online service produces a target that goes active immediately and a fix that does not fix anything - Enable the matching wait-online service.
- ``
sudo systemctl enable systemd-networkd-wait-online.service`` - Add both halves of the dependency in a drop-in, so a package update does not overwrite it:
- ``
sudo systemctl edit api.service`` - ``
[Unit] Wants=network-online.target After=network-online.target`` - Remove the sleep. Leaving it in hides whether the real fix works and adds ten seconds to every boot
- Reload and confirm the rendered dependencies changed, not just the file on disk:
sudo systemctl daemon-reloadthensystemctl show -p After -p Wants api.service - If the address is a floating VIP rather than a host address, wait-online cannot help — the VIP may legitimately live on another node. Enable non-local bind instead:
- ``
echo 'net.ipv4.ip_nonlocal_bind = 1' | sudo tee /etc/sysctl.d/60-nonlocal-bind.conf sudo sysctl --system`` - **Add
Restart=on-failurewith aRestartSec** as a backstop for whatever race remains, and accept that it is a backstop and not the fix
Verification
- Both halves are present in the rendered unit.
systemctl show -p After -p Wants api.servicelistsnetwork-online.targetunder each. Checking the file instead of the rendered output is how this was missed the first time - The target actually activates.
systemctl is-active network-online.targetreturnsactiveon a booted host. If it returnsinactive, nothing is waiting and the ordering is still against a unit that never runs - The wait-online service ran and blocked.
systemd-analyze blame | grep wait-onlineshows a non-trivial duration on a host where DHCP takes time - The critical chain now includes the network.
systemd-analyze critical-chain api.servicelistsnetwork-online.targetahead of the service - Reboot at least five times.
systemctl is-failed api.servicemust returnnoafter every one. A one-in-four race has a 32% chance of surviving three reboots undetected, so a single clean boot is not evidence - Prove the failure mode is gone rather than hidden. Delay DHCP deliberately — shut the switch port for 20 seconds after power-on, or add a delay on the DHCP server — and confirm the service still comes up rather than failing faster
Prevention
- Read
After=andWants=as two separate statements, because they are. Ordering says when if both run; the requirement dependency says whether it runs at all.After=alone against a target nothing pulls in is the single most common systemd ordering bug. - Verify with
systemctl show -p After -p Wants, never by reading the unit file. The rendered view accounts for drop-ins, generators and defaults; the file does not. - Check
systemctl is-active network-online.targeton a booted host as a standing fleet assertion.inactivemeans every unit ordered after it is unordered. - Ban
sleepfrom unit files in review. If a wait is unavoidable, poll the real condition with a bounded loop that fails loudly. - Any bug reported as intermittent needs a boot-loop or a restart-loop in acceptance. A race that fires 25% of the time will pass a single-run test three times out of four.