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:
- Matching service required —
foo.socketneedsfoo.serviceby default (Service=can override). Depending onAccept=, that is a plain unit or afoo@.servicetemplate (next section). - 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 explicitRequires=(and usuallyAfter=) on the service if you want to prevent that. - Daemon must accept handed FDs — via the native protocol (sd_listen_fds(3), FDs typically from 3) or inetd-style (
StandardInput=socket). - Default dependencies — unless
DefaultDependencies=no, socket units get aBefore=onsockets.target(and related boot/shutdown edges). A common[Install]pattern isWantedBy=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 unit | FD passed | Notes (man) |
|---|---|---|---|
no (default) | foo.service | Listening sockets | Preferred for performance-sensitive daemons — only the first connection pays activation cost |
yes | foo@.service instances | Connection socket only | Per-connection process/sandbox; useful for inetd-style programs |
| (datagram / FIFO) | — | Setting ignored — one service handles all traffic | Explicit in man |
More:
- New daemons should be written for
Accept=no(man recommendation). Multiplexing lives in the service. - AF_UNIX — may
close(2)the received socket before exit, but must not unlink it from the filesystem. Do notshutdown(2)sockets received withAccept=no; allowed forAccept=yes. - IPv4/IPv6 — activated services may get
$REMOTE_ADDR/$REMOTE_PORT(man references CGI / RFC 3875). - Native passing — with
Accept=no, useLISTEN_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 |
|---|---|---|---|
| Trigger | OnCalendar= (etc.) | PathExists= / PathChanged= (etc.) | ListenStream= (etc.) + inbound traffic |
| Typical question | Every day at 03:00? | When this file appears? | When something connects to this port/UNIX socket? |
| Internals | timerfd / calendar | inotify | Kernel socket/FIFO bound and listened by systemd |
| Common install target | timers.target | paths.target | sockets.target |
| “Delay start” means | Wait until next schedule | Wait until path condition | Listen 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) —
.socketdescription,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)