Skip to main content
RunBook Academy

← All break/fix scenarios in Linux

advancedServices~40 min

Break/Fix: the service fails on one boot in four and starts fine by hand

Reported symptoms

  • A service fails to start on roughly one boot in four and starts perfectly when restarted by hand
  • The failure message is `Cannot assign requested address` at bind time
  • The application team says nothing changed and the same binary runs fine on every other host
  • The fleet was fine for two weeks after a firmware update, then the failures returned
  • A previously added `After=network-online.target` did not help

Evidence

  • · `systemctl status api.service` after a failed boot reports `Failed with result exit-code` and exit status 1
  • · `sudo journalctl -u api -b` shows `bind 192.0.2.50:8443: cannot assign requested address`
  • · `systemctl show -p After api.service` lists `network-online.target` among its ordering dependencies
  • · `systemctl show -p Wants -p Requires api.service` does not list `network-online.target`
  • · `systemctl is-active network-online.target` reports `inactive` on a running, fully networked host
  • · `systemctl is-enabled systemd-networkd-wait-online.service` reports `disabled`
  • · `systemd-analyze critical-chain api.service` shows it starting 1.4s after `basic.target` with no network unit in the chain
  • · `sudo journalctl -b -u systemd-networkd` shows the address configured 0.6s after api.service exited
Diagnosis and resolutionclick to reveal

Root cause

The unit is ordered after `network.target`, which only means that network management has been started — not that any interface has an address. The application binds to a specific address, so it needs the address to exist, and on boots where systemd reaches the unit before DHCP completes the bind fails with EADDRNOTAVAIL. The race is decided by unrelated boot work, which is why it is intermittent and why it came back when a firmware update shortened POST. The apparent fix compounded it: `After=network-online.target` is only an *ordering* statement. A target that nothing pulls in never activates, and an ordering dependency on an inactive unit is satisfied immediately, so the line reads like a fix and does nothing. The target also needs a `Wants=`, and the corresponding `wait-online` service must be enabled, or there is nothing to wait for.

Remediation

Add both halves of the dependency — `Wants=network-online.target` and `After=network-online.target` — in a drop-in, and enable the wait-online service that matches the network manager in use (`systemd-networkd-wait-online.service` or `NetworkManager-wait-online.service`). Reload and confirm the target actually becomes active. Where the address is a floating VIP that wait-online can never guarantee, stop waiting for it: set `net.ipv4.ip_nonlocal_bind=1` so the process can bind an address the host does not hold yet, or move the listener to a systemd socket unit so systemd owns the bind. Adding `Restart=on-failure` is worth having as a backstop but is not the fix — it converts a hard failure into a slower one.

Verification

`systemctl show -p After -p Wants api.service` must list `network-online.target` under both. `systemctl is-active network-online.target` must report `active` on a booted host — if it reports `inactive`, the ordering is still against nothing. `systemd-analyze critical-chain api.service` must show a network unit in the chain ahead of the service. Then reboot the host at least five times in a row and confirm `systemctl is-failed api.service` reports `no` every time; a race that fails one boot in four cannot be verified by a single successful boot.

Prevention

Treat `network.target` as "the network stack is being configured" and `network-online.target` as "an address exists", and never write an `After=` on a target without the matching `Wants=` unless something else is already pulling that target in. Check the rendered result with `systemctl show -p After -p Wants` rather than trusting the unit file. Prefer explicit readiness — socket activation, `RequiresMountsFor=`, a health-checked `ExecStartPre` — over `sleep`, which encodes a guess about a machine that will get faster. Add a boot-loop test to acceptance for any service whose failures are intermittent: one reboot proves nothing about a race.

Reported symptoms

  • api.service fails 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 api works 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.target to 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:

  1. systemctl show -p After does list network-online.target. So the ordering line is present and systemd parsed it.
  2. systemctl is-active network-online.target says inactive — on a host that is up, networked and serving traffic.
  3. systemd-analyze critical-chain does 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.service starts 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 is After= 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

  1. Establish which network manager actually owns the interface. networkctl status eth0 for systemd-networkd, nmcli device show eth0 for NetworkManager. Enabling the wrong wait-online service produces a target that goes active immediately and a fix that does not fix anything
  2. Enable the matching wait-online service.
  3. `` sudo systemctl enable systemd-networkd-wait-online.service ``
  4. Add both halves of the dependency in a drop-in, so a package update does not overwrite it:
  5. `` sudo systemctl edit api.service ``
  6. `` [Unit] Wants=network-online.target After=network-online.target ``
  7. Remove the sleep. Leaving it in hides whether the real fix works and adds ten seconds to every boot
  8. Reload and confirm the rendered dependencies changed, not just the file on disk: sudo systemctl daemon-reload then systemctl show -p After -p Wants api.service
  9. 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:
  10. `` echo 'net.ipv4.ip_nonlocal_bind = 1' | sudo tee /etc/sysctl.d/60-nonlocal-bind.conf sudo sysctl --system ``
  11. **Add Restart=on-failure with a RestartSec** as a backstop for whatever race remains, and accept that it is a backstop and not the fix

Verification

  1. Both halves are present in the rendered unit. systemctl show -p After -p Wants api.service lists network-online.target under each. Checking the file instead of the rendered output is how this was missed the first time
  2. The target actually activates. systemctl is-active network-online.target returns active on a booted host. If it returns inactive, nothing is waiting and the ordering is still against a unit that never runs
  3. The wait-online service ran and blocked. systemd-analyze blame | grep wait-online shows a non-trivial duration on a host where DHCP takes time
  4. The critical chain now includes the network. systemd-analyze critical-chain api.service lists network-online.target ahead of the service
  5. Reboot at least five times. systemctl is-failed api.service must return no after every one. A one-in-four race has a 32% chance of surviving three reboots undetected, so a single clean boot is not evidence
  6. 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= and Wants= 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.target on a booted host as a standing fleet assertion. inactive means every unit ordered after it is unordered.
  • Ban sleep from 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.