A dependency graph needs to express more than “start the database first.” One relationship decides whether another unit is pulled into the transaction; another orders the jobs; the service type defines when startup has finished. Application readiness is a further promise that the program must actually fulfill. Confusing these layers produces a boot sequence that looks ordered while the dependent service still starts too early.

When it breaks

After=provider.service orders this unit after the provider when both have start jobs. It does not request that the provider start. If only the consumer is activated, the ordering edge alone can leave the provider inactive. Add an appropriate activation dependency when the consumer really should bring the provider into the transaction.

Wants requests another unit but tolerates its startup failure. Requires expresses a stronger relationship; together with After, a provider that fails activation prevents the dependent unit from starting. Neither keyword by itself establishes startup order. Use the combination that matches the service’s actual need rather than turning every optional component into a hard prerequisite.

Ordered startup is only as strong as the provider’s completion signal. Type=exec establishes successful program execution, not completion of database initialization. Type=notify can hold startup until the program sends READY=1; a oneshot can hold it until finite setup finishes. The application must send notification only when its promised readiness condition is true.

Check it quickly

Run this complete Python script as an administrator in a disposable Linux systemd environment. It installs two temporary runtime units and exercises six configurations sequentially. The recorded run used Debian 12 and systemd 252.39. Ensure both example unit names are unused. Temporary files make the provider’s process entry, readiness, and the consumer’s observation independently visible:

from pathlib import Path
from tempfile import TemporaryDirectory
import subprocess,time

provider='xbcatm-provider-lab.service';consumer='xbcatm-consumer-lab.service'
paths={name:Path('/run/systemd/system')/name for name in (provider,consumer)}
assert all(not p.exists() for p in paths.values())
def ctl(*args,success=True):
    result=subprocess.run(['systemctl',*args],text=True,capture_output=True,timeout=10)
    assert (result.returncode==0)==success,(args,result.stderr)
    return result.stdout.strip()
def value(name,key):return ctl('show',name,'-p',key,'--value')
def wait_for(check):
    deadline=time.monotonic()+5
    while not check():
        assert time.monotonic()<deadline,'state transition timed out'
        time.sleep(0.02)
with TemporaryDirectory(prefix='xbcatm-dependency-',dir='/run') as directory:
    root=Path(directory)
    (root/'provider.py').write_text(r"""from pathlib import Path
import os,socket,sys,time
root=Path(__file__).parent;mode=(root/'mode').read_text()
if mode=='fail': sys.exit(7)
(root/'entered').touch()
if mode in ('exec','notify'):
    while not (root/'gate').exists(): time.sleep(0.01)
(root/'ready').touch()
if mode=='notify':
    address=os.environ['NOTIFY_SOCKET']
    if address.startswith('@'): address='\0'+address[1:]
    with socket.socket(socket.AF_UNIX,socket.SOCK_DGRAM) as notification:
        notification.connect(address);notification.sendall(b'READY=1')
if mode in ('exec','notify'): time.sleep(60)
""")
    (root/'consumer.py').write_text("from pathlib import Path\nr=Path(__file__).parent\n(r/'observed').write_text(str((r/'ready').exists()))\n")
    def install(mode,requirement):
        (root/'mode').write_text(mode)
        kind='oneshot' if mode in ('oneshot','fail') else mode
        paths[provider].write_text(f'[Service]\nType={kind}\nExecStart=/usr/bin/python3 {root}/provider.py\nTimeoutStartSec=5s\n'+('RemainAfterExit=yes\n' if kind=='oneshot' else ''))
        paths[consumer].write_text(f'[Unit]\nAfter={provider}\n'+(f'{requirement}={provider}\n' if requirement else '')+f'[Service]\nType=oneshot\nExecStart=/usr/bin/python3 {root}/consumer.py\n')
        subprocess.run(['systemd-analyze','verify',*[str(p) for p in paths.values()]],check=True)
        ctl('daemon-reload')
    def reset():
        ctl('stop',consumer,provider)
        for name in paths:
            if value(name,'ActiveState')=='failed':ctl('reset-failed',name)
        for name in ('entered','gate','ready','observed'):(root/name).unlink(missing_ok=True)
    pending=None
    install('oneshot','')
    try:
        ctl('start',consumer)
        assert (root/'observed').read_text()=='False' and value(provider,'ActiveState')=='inactive'
        print('After alone: provider not pulled in; consumer ran before readiness')
        reset();install('fail','Wants');ctl('start',consumer)
        assert value(provider,'ActiveState')=='failed' and (root/'observed').read_text()=='False'
        print('Wants plus After: consumer runs despite provider failure')
        reset();install('fail','Requires');ctl('start',consumer,success=False)
        assert value(provider,'ActiveState')=='failed' and not (root/'observed').exists()
        print('Requires plus After: failed provider prevents consumer execution')
        reset();install('oneshot','Requires');ctl('start',consumer)
        assert (root/'observed').read_text()=='True'
        print('oneshot provider: consumer sees completed setup')
        reset();install('exec','Requires');ctl('start',consumer)
        assert (root/'observed').read_text()=='False' and value(provider,'ActiveState')=='active'
        (root/'gate').touch();wait_for(lambda:(root/'ready').exists())
        print('Type=exec provider: process started before application readiness')
        reset();install('notify','Requires')
        pending=subprocess.Popen(['systemctl','start',consumer],text=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
        wait_for(lambda:(root/'entered').exists())
        assert pending.poll() is None and not (root/'observed').exists()
        assert value(provider,'ActiveState')=='activating'
        (root/'gate').touch();out,err=pending.communicate(timeout=10)
        assert pending.returncode==0,err
        assert (root/'observed').read_text()=='True'
        print('Type=notify provider: dependent waits for actual READY=1')
    finally:
        reset()
        if pending is not None and pending.poll() is None:pending.communicate(timeout=5)
        for path in paths.values():path.unlink()
        ctl('daemon-reload')

With only After, the consumer runs and records false for readiness while the provider remains inactive. That is a missing activation relationship, not a slow provider. With Wants plus After, the provider deliberately exits with status seven, but the consumer still runs and records false. The fixture accepts that observation; a real consumer might then fail its own work if the dependency was actually mandatory.

With Requires plus After, the same provider failure causes the consumer’s start command to fail and no observation file appears. The absent file proves the consumer program never ran. Replacing the failing provider with successful oneshot setup produces a true observation because ordering now waits for that setup to complete.

The next case uses Type=exec. The provider process is active but deliberately waits at an application gate before publishing readiness. The consumer still starts and records false. Releasing the gate later makes the provider ready, demonstrating the timing gap without depending on an unreliable arbitrary startup sleep.

Finally, Type=notify keeps the provider activating while it waits at the same gate. The consumer’s start job remains pending, and its program has not executed. After the gate opens, the provider publishes readiness and sends the actual systemd notification from its main process. Only then does the consumer run and record true.

The script verifies each unit configuration and cleans up both services, including a pending job if an assertion interrupts the exercise. These outcomes measure manager behavior with real processes and a real notification socket. They do not establish that a production database’s readiness definition is sufficient for every client operation.

Match dependencies to failure semantics

Choose a readiness definition with the consumers in mind. A listening port may exist before migrations, credentials, or required data are ready. Conversely, waiting for an optional remote dependency can make a local service unavailable unnecessarily. Name the specific condition and decide which failures should block startup versus be handled during normal operation.

A completed startup does not guarantee that the provider stays healthy forever. Ordinary requirement dependencies are not continuous business-health checks, and condition-based skips differ from failed activation. Plan how the application handles later disconnects, restarts, and unavailable external services rather than treating the boot graph as a permanent availability guarantee.

Inspect the loaded unit definitions, their effective dependencies, and the relevant startup results when diagnosing order. Reload the manager after changing unit files, then test the failure case as well as a clean boot. Keep unrelated services independent so they can start concurrently, while retaining only the ordering edges needed by the actual completion contract.

Sources