systemd Socket Activation: Delay-Start a Service

systemd socket activation keeps the daemon process off the boot critical path: systemd binds and listens first, then starts the matching .service when traffic arrives. Unit shape and directives are defined in man systemd.socket (systemd.socket(5)).

This post covers only What is a socket unit? · What does Accept= mean? · How does it differ from a timer?. systemd-timer-oncalendar is the calendar / schedule axis; systemd-path-unit is the path condition (inotify) axis. Here the axis is delay-starting a service when an IPC/network socket or FIFO sees traffic. No affiliate, pricing, or invented field stories.

Sources: systemd.socket(5), sd_listen_fds(3); contrast only systemd.timer(5) and systemd.path(5).

What is a socket unit?

One-line answer: A unit whose name ends in .socket describes a socket or FIFO supervised by systemd and starts a matching .service on incoming traffic. You usually enable the .socket, not the service.

From the man page:

  1. Matching service required — foo.socket needs foo.service by default (Service= can override). Depending on Accept=, that is a plain unit or a foo@.service template (next section).
  2. No implicit WantedBy/RequiredBy — systemd does not add an automatic WantedBy= / RequiredBy= from the socket to the service. Starting the service alone means it must open sockets itself; add an explicit Requires= (and usually After=) on the service if you want to prevent that.
  3. Daemon must accept handed FDs — via the native protocol (sd_listen_fds(3), FDs typically from 3) or inetd-style (StandardInput=socket).
  4. Default dependencies — unless DefaultDependencies=no, socket units get a Before= on sockets.target (and related boot/shutdown edges). A common [Install] pattern is WantedBy=sockets.target.

Minimal sketch (Accept=no, TCP loopback):

# /etc/systemd/system/demo-echo.socket
[Unit]
Description=Demo echo socket (delay-start service)

[Socket]
ListenStream=127.0.0.1:12345
Accept=no

[Install]
WantedBy=sockets.target
# /etc/systemd/system/demo-echo.service
[Unit]
Description=Demo echo service (socket-activated)
Requires=demo-echo.socket
After=demo-echo.socket

[Service]
ExecStart=/usr/local/bin/demo-echo
# Daemon must use the listening FD(s) from sd_listen_fds

Apply:

sudo systemctl daemon-reload
sudo systemctl enable --now demo-echo.socket
systemctl status demo-echo.socket
# The service often stays inactive until the first connection

ListenStream= / ListenDatagram= / ListenSequentialPacket= address forms (man summary): /path → filesystem AF_UNIX; @name → abstract UNIX; bare port → IPv6 (and possibly IPv4 per BindIPv6Only=); v.w.x.y:z → IPv4; [x]:y → IPv6.

vs path / timer: not “when on the calendar?” or “what appeared on this path?” — who connected to this listen address?

What does Accept= mean?

One-line answer: Accept=no (default) hands the listening sockets to one service process that accepts connections. Accept=yes spawns a template instance per connection and passes only the connected socket.

Accept=Default peer unitFD passedNotes (man)
no (default)foo.serviceListening socketsPreferred for performance-sensitive daemons — only the first connection pays activation cost
yesfoo@.service instancesConnection socket onlyPer-connection process/sandbox; useful for inetd-style programs
(datagram / FIFO)—Setting ignored — one service handles all trafficExplicit in man

More:

  1. New daemons should be written for Accept=no (man recommendation). Multiplexing lives in the service.
  2. AF_UNIX — may close(2) the received socket before exit, but must not unlink it from the filesystem. Do not shutdown(2) sockets received with Accept=no; allowed for Accept=yes.
  3. IPv4/IPv6 — activated services may get $REMOTE_ADDR / $REMOTE_PORT (man references CGI / RFC 3875).
  4. Native passing — with Accept=no, use LISTEN_FDS / sd_listen_fds; first FD is commonly 3 (SD_LISTEN_FDS_START).

Naming for Accept=yes: foo.socket → instances of foo@.service.

How does it differ from a timer?

One-line answer: Schedule → .timer; path condition → .path; socket/FIFO traffic → .socket. All three can start a peer service; the trigger axis differs.

Axis.timer.path.socket
TriggerOnCalendar= (etc.)PathExists= / PathChanged= (etc.)ListenStream= (etc.) + inbound traffic
Typical questionEvery day at 03:00?When this file appears?When something connects to this port/UNIX socket?
Internalstimerfd / calendarinotifyKernel socket/FIFO bound and listened by systemd
Common install targettimers.targetpaths.targetsockets.target
“Delay start” meansWait until next scheduleWait until path conditionListen first; start the process on demand

Contrast only:

  • systemd-timer-oncalendar — no retread of OnCalendar= / Persistent= here.
  • systemd-path-unit — PathExists/PathChanged and path journal checks stay in that post.
  • This post — prepare listening FDs at boot; defer daemon RSS/startup until demand. Need a calendar job with no socket → timer. Drop directory file → path. Client hits a port → socket.

FAQ

Should I enable the service too?

Usually enable only the .socket. The socket starts the service on traffic. You can systemctl start demo-echo.service to test, but then the service must open sockets itself or pull the socket via Requires= (man: no implicit WantedBy).

Can I combine path/timer with the same job?

Possible, but overlapping triggers can double-start work. Prefer one primary trigger. Read the sibling posts when the axis differs.

Accept=yes but only foo.service exists?

Per man naming, Accept=yes needs a foo@.service template. A non-template foo.service alone will not instantiate per connection.

Why care on embedded?

For rarely used admin/debug listeners, keeping only the listening FD ready saves steady-state memory/CPU. Board-specific numbers are out of scope.

Sources

  • systemd.socket(5) — .socket description, Accept=, Listen*, peer naming, default deps (sockets.target)
  • sd_listen_fds(3) — native FD passing
  • Adjacent axes (no retread): systemd-timer-oncalendar (calendar), systemd-path-unit (path/inotify)