Socket activation lets systemd open a listening endpoint before the application process starts. Activity on that endpoint can then trigger a compatible service, which receives descriptors for sockets the manager already owns. The useful boundary is listener ownership versus application lifetime. An open endpoint is not yet proof that the application has initialized, accepted a request, or produced a correct response.

When it breaks

The application must understand the activation protocol. A daemon that unconditionally binds the same endpoint again can fail because the manager already occupies it. Activation moves binding responsibility into the socket unit; it does not eliminate address conflicts, missing interfaces, filesystem permission errors, or other reasons that initial bind can fail.

With Accept=no, the service receives the listening socket and handles acceptance itself. Accept=yes instead supports a per-connection service-instance model for suitable stream sockets. These are different application contracts. Neither mode implies that an arbitrary daemon will automatically become idle, exit after work, or restart only when doing so is safe.

Queued connections also have limits. Clients can establish transport state before application startup finishes, then time out waiting for a response. Socket activation does not make database dependencies ready or create an unlimited request queue. Keep explicit service dependencies, readiness behavior, and client timeouts appropriate to the real workload.

Check it quickly

Run this Python script as an administrator in a disposable Linux system whose PID 1 is systemd. The recorded environment was Debian 12 with systemd 252.39. It creates a unique socket/service pair using a Unix stream socket in a temporary directory. No public TCP port is exposed. The worker intentionally serves one newline-terminated message and exits:

from pathlib import Path
from tempfile import TemporaryDirectory
import json,socket,subprocess,time,uuid

name='xbcatm-socket-'+uuid.uuid4().hex[:8];service=name+'.service';unit=name+'.socket'
paths={n:Path('/run/systemd/system')/n for n in (service,unit)}
assert all(not p.exists() for p in paths.values())
def run(*args):
    result=subprocess.run(args,capture_output=True,text=True,timeout=10)
    assert result.returncode==0,(args,result.stderr)
    return result.stdout.strip()
def value(key):return run('systemctl','show',service,'-p',key,'--value')
def wait_inactive():
    deadline=time.monotonic()+5
    while value('ActiveState')!='inactive':
        assert time.monotonic()<deadline,'service did not exit';time.sleep(0.02)
with TemporaryDirectory(prefix=name+'-',dir='/run') as directory:
    root=Path(directory);endpoint=root/'listener.sock';worker=root/'worker.py'
    worker.write_text(r'''from pathlib import Path
import errno,json,os,socket
root=Path(__file__).parent
assert int(os.environ['LISTEN_PID'])==os.getpid()
assert int(os.environ['LISTEN_FDS'])==1
with socket.socket(fileno=3) as listener:
    assert listener.getsockopt(socket.SOL_SOCKET,socket.SO_ACCEPTCONN)==1
    with socket.socket(socket.AF_UNIX,socket.SOCK_STREAM) as duplicate:
        try:duplicate.bind(str(root/'listener.sock'))
        except OSError as error:assert error.errno==errno.EADDRINUSE
        else:raise AssertionError('unexpected duplicate bind success')
    connection,_=listener.accept()
    with connection,connection.makefile('rb') as incoming:
        message=incoming.readline(256);assert message.endswith(b'\n')
        connection.sendall(b'ack '+message)
    (root/(str(os.getpid())+'.json')).write_text(json.dumps({'pid':os.getpid(),'fd':3,'message':message.decode().strip()}))
''')
    paths[unit].write_text(f'[Socket]\nListenStream={endpoint}\nAccept=no\nSocketMode=0600\nRemoveOnStop=yes\n')
    paths[service].write_text(f'[Service]\nType=exec\nExecStart=/usr/bin/python3 {worker}\n')
    try:
        run('systemd-analyze','verify',*[str(p) for p in paths.values()]);run('systemctl','daemon-reload')
        run('systemctl','start',unit)
        assert endpoint.is_socket() and value('ActiveState')=='inactive' and value('MainPID')=='0'
        print('socket listening while service has no process')
        for message in ('first','second'):
            with socket.socket(socket.AF_UNIX,socket.SOCK_STREAM) as client:
                client.settimeout(5);client.connect(str(endpoint));client.sendall((message+'\n').encode())
                with client.makefile('rb') as response:assert response.readline()==('ack '+message+'\n').encode()
            wait_inactive();assert endpoint.is_socket() and value('Result')=='success'
        records=[json.loads(p.read_text()) for p in root.glob('*.json')]
        assert len(records)==2 and len({row['pid'] for row in records})==2
        assert {row['message'] for row in records}=={'first','second'}
        assert all(row['fd']==3 for row in records)
        print('two requests: two deliberate one-request workers; each uses inherited listening fd 3')
        print('rebinding the occupied socket fails; manager retains listener between workers')
        run('systemctl','stop',unit);assert not endpoint.exists()
        print('stopping socket removes its endpoint with RemoveOnStop=yes')
    finally:
        run('systemctl','stop',unit,service)
        for name,path in paths.items():
            if run('systemctl','show',name,'-p','ActiveState','--value')=='failed':run('systemctl','reset-failed',name)
            path.unlink()
        run('systemctl','daemon-reload')

After starting only the socket unit, the endpoint exists while the corresponding service is inactive and has no main PID. A first client connection then causes the worker to execute. The client sends a complete framed message and checks an application reply, rather than treating a successful connect call as proof of usable service.

The worker verifies that LISTEN_PID names its own process and LISTEN_FDS reports one inherited descriptor. It wraps descriptor three and checks that it is already listening. The numbering and environment checks match the native activation contract. A production program handling multiple descriptors should also identify and validate the sockets it actually expects.

Attempting another bind to the same Unix socket path produces EADDRINUSE. The worker continues using the inherited listener and returns the expected reply. This controlled failure demonstrates why an application cannot simply retain its ordinary bind path while assuming activation will somehow avoid the conflict.

After the worker exits successfully, the manager still holds the listening endpoint. A second connection launches another worker, which handles the second message and exits. The recorded PIDs are different and both invocations used descriptor three. Two processes appear because this specific worker deliberately exits after one request; Accept=no did not itself require one process per connection.

Stopping the socket unit removes the endpoint because the fixture explicitly sets RemoveOnStop=yes. The script then stops any remaining test units and removes their runtime definitions and temporary files. This cleanup behavior is configured and observed, not a universal promise that every socket path disappears whenever any associated daemon exits.

Design the handoff and shutdown together

For a long-running daemon, accept and serve the inherited sockets using its documented integration, and let its normal shutdown behavior decide when descriptors close. For a daemon intended to leave when idle, verify the idle-exit logic and what happens to in-flight requests. systemd cannot infer when an application’s work is safely complete.

Stopping future activation and terminating current work are separate operational decisions. Inspect both socket and service units during maintenance. A service may be started explicitly through another dependency or by an operator, and a still-active listener may trigger it again. Test the deployed unit relationships rather than assuming that stopping one visible process permanently disables the endpoint.

Set appropriate endpoint permissions and service identity. Passing an already-open descriptor allows listener setup and application privileges to differ, but it does not make the service a sandbox. Verify real request handling, restart behavior, queued-client outcomes, and failure diagnostics. The practical advantage is a deliberate, testable handoff between listener and process, not automatic readiness or zero resource cost.

Further reading

Sources