A daemon normally binds its own port during startup. With systemd socket activation, systemd opens the listening socket first, keeps it independent of the process lifecycle, and starts the service when traffic arrives. This enables on-demand startup, lets an unprivileged daemon serve a privileged port, and preserves the queue of new connections while the service restarts.
This guide implements a .socket/.service pair, receives file descriptors with sd_listen_fds(), distinguishes Accept=no from Accept=yes, and covers hardening, rollout, and debugging. It follows official systemd sources for socket units, the socket activation API, and systemd-socket-activate.
1. Socket activation changes port ownership
In the traditional model, a daemon calls socket(), bind(), listen(), and accept(). If the daemon is down or restarting, no process owns the listening socket.
With socket activation:
- systemd creates and owns the listening socket from a
.socketunit. - Incoming traffic activates the matching
.service. - systemd passes the already-open file descriptor to the process.
- The daemon uses that descriptor instead of binding the address again.
The default names match: report-api.socket activates report-api.service. Service= can override this with Accept=no, but matching names are easier to operate.
2. A minimal TCP socket unit
# /etc/systemd/system/report-api.socket
[Unit]
Description=Report API listening socket
[Socket]
ListenStream=127.0.0.1:9100
Accept=no
Backlog=256
NoDelay=yes
FileDescriptorName=http
[Install]
WantedBy=sockets.target
ListenStream= creates a TCP stream socket when given an IP address and port. Accept=no is the default: the service receives the listening socket and calls accept() for clients. FileDescriptorName= helps a service identify multiple passed sockets without relying only on order.
Loopback is appropriate when a reverse proxy on the same host sits in front. Binding 0.0.0.0 or [::] can expose the service externally; socket activation does not replace a firewall or access control.
3. An unprivileged service unit
# /etc/systemd/system/report-api.service
[Unit]
Description=Report API service
Requires=report-api.socket
After=report-api.socket network.target
[Service]
Type=simple
User=report-api
Group=report-api
ExecStart=/usr/local/bin/report-api
Restart=on-failure
RestartSec=2s
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/var/lib/report-api
[Install]
WantedBy=multi-user.target
PID 1 binds the port and passes the descriptor, so the daemon does not need a capability merely to open a low port. It still needs deliberate permissions for configuration, data writes, and outbound networking under the real sandbox.
For a purely on-demand service, enable and start the socket rather than the service. An always-running service can still be enabled, but the application needs a clear policy for activated sockets and for execution outside systemd.
4. Receive the file descriptor in C
#include <systemd/sd-daemon.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int count = sd_listen_fds(1);
if (count < 0) {
fprintf(stderr, "sd_listen_fds failed: %d\n", count);
return EXIT_FAILURE;
}
if (count != 1) {
fprintf(stderr, "expected one activated socket, got %d\n", count);
return EXIT_FAILURE;
}
int listen_fd = SD_LISTEN_FDS_START; /* file descriptor 3 */
if (sd_is_socket_inet(listen_fd, AF_UNSPEC, SOCK_STREAM, 1, 9100) != 1) {
fprintf(stderr, "unexpected socket\n");
return EXIT_FAILURE;
}
for (;;) {
int client = accept4(listen_fd, NULL, NULL, SOCK_CLOEXEC);
if (client < 0) continue;
handle_client(client);
close(client);
}
}
The native protocol places descriptors consecutively from SD_LISTEN_FDS_START, normally FD 3, and provides LISTEN_PID, LISTEN_FDS, and LISTEN_FDNAMES. Use libsystemd instead of blindly trusting FD 3: helpers validate that the descriptors target the current PID and can clear the environment after reading it.
5. Multiple sockets and descriptor names
[Socket]
ListenStream=127.0.0.1:9100
ListenStream=/run/report-api/admin.sock
SocketMode=0660
SocketUser=report-api
SocketGroup=report-admin
FileDescriptorName=report-api
A unit may declare multiple sockets; all of them are passed when its service activates. For a clear public/admin split, use two socket units with separate FileDescriptorName= values and let the service call sd_listen_fds_with_names(). Do not depend on ordering across socket units because the documentation does not guarantee it.
For filesystem Unix sockets, SocketMode=, SocketUser=, and SocketGroup= enforce access at the socket node. They are not equivalent controls for TCP.
6. Accept=no versus Accept=yes
| Mode | What the service receives | Typical fit |
|---|---|---|
Accept=no | Listening socket | HTTP servers, high-throughput daemons, event loops |
Accept=yes | One connected socket per instance | Sporadic services, inetd-style programs, per-connection isolation |
With Accept=yes, echo.socket needs an echo@.service template, and systemd creates an instance for every connection. This simplifies a single-session program but process and unit costs grow quickly. The systemd documentation recommends Accept=no for performance-sensitive services because only the first connection pays activation cost.
Datagram sockets and FIFOs are always handled by one service; Accept= does not provide the same per-connection behavior there.
7. What does a restart preserve?
When only the service stops while the socket unit remains active, systemd continues owning the listening socket. New connections can wait in the listen backlog and activate the service again. This reduces the connection refused window while the process is unavailable.
Socket activation does not migrate connections that the old daemon already accepted. Those connections still belong to the old process. A no-disruption deployment for HTTP keep-alive, WebSocket, or long sessions needs graceful shutdown: stop accepting new work, drain existing connections to a deadline, and terminate afterward. A reverse proxy, multiple instances, or dedicated FD handoff may also be required.
Do not advertise “zero downtime” merely because the port stays open. Measure success rate and latency during restarts.
8. Activation storms, backlog, and limits
A service that fails immediately can be repeatedly activated by traffic. Combine:
StartLimitIntervalSec=andStartLimitBurst=in unit/service policy.TriggerLimitIntervalSec=andTriggerLimitBurst=for appropriate sockets.Restart=on-failurewith an intentional delay.- Alerts on activation failure instead of leaving clients to observe only timeouts.
Backlog= is not an infinite queue. If startup is slow or a burst exceeds capacity, clients can still time out or be rejected. Size it with load tests, kernel limits, and SLOs; never use backlog to hide an under-capacity service.
9. Security at the socket boundary
- Bind the narrowest address possible: loopback or a Unix socket for local-proxy access.
- Set strict owner, group, and mode for filesystem sockets.
- Run the daemon as a dedicated user rather than root solely for port binding.
- Use a firewall for TCP and authentication in the application or reverse proxy.
- Apply
NoNewPrivileges, namespaces, and filesystem policy after testing. - Do not treat
REMOTE_ADDRas identity; a network address is not authentication.
With Accept=yes, sandboxing each service instance can isolate connections more strongly, but process, memory, and activation limits are required to prevent exhaustion.
10. Installation and rollout
sudo systemd-analyze verify \
/etc/systemd/system/report-api.socket \
/etc/systemd/system/report-api.service
sudo systemctl daemon-reload
sudo systemctl enable --now report-api.socket
systemctl status report-api.socket
systemctl list-sockets --all | grep report-api
curl http://127.0.0.1:9100/health
Immediately after enable --now, the socket should be listening while the service may remain inactive. The first request activates it. A safe rollout proves this sequence in staging, then watches startup latency and the first response in production.
For maintenance that must not reactivate the service, stop the socket before the service. Stopping only the service while the socket stays active can let the next connection start it again.
11. Test the daemon before installing units
systemd-socket-activate \
--listen=127.0.0.1:9100 \
--fdname=http \
/usr/local/bin/report-api
systemd-socket-activate simulates descriptor passing from the command line. Test no descriptors, the wrong count, an incorrect socket type or port, multiple named descriptors, service crashes, restarts, and bursts before readiness. An application that requires activation should fail clearly when descriptors are invalid.
12. Observability and debugging
systemctl status report-api.socket report-api.service
systemctl show report-api.socket -p ActiveState -p Triggers
journalctl -u report-api.socket -u report-api.service --since today
ss -ltnp 'sport = :9100'
systemctl list-sockets --all
Separate three failure layers: the socket is not listening, the service does not activate, or the service activates but mishandles its descriptor. Metrics should include activation count and failure, time from first connection to ready, backlog pressure, request latency/errors during restart, and graceful shutdowns that exceed their deadline.
“Address already in use” usually means the application still binds the address instead of consuming the passed descriptor. A hanging request often means the daemon is not calling accept() on the correct listening FD.
Production checklist
- The
.socket/.servicenames orService=mapping are correct. - The daemon calls
sd_listen_fds()and validates the socket instead of blindly hardcoding FD 3. Accept=noorAccept=yesmatches actual concurrency and isolation needs.- The listening address, firewall, and Unix socket permissions have the intended scope.
- The service runs under a dedicated user and hardening permits required resources.
- Backlog, restart policy, and trigger/start limits are load-tested.
- Graceful draining covers accepted connections; socket activation is not mistaken for connection migration.
- Cold starts, crash loops, bursts, maintenance stops, and rollback are tested.
- Dashboards distinguish socket state, activation failure, and application failure.
Conclusion
systemd socket activation separates the lifetime of a listening socket from the lifetime of a daemon. A service can start on traffic, run unprivileged, and restart while systemd preserves the port and queue of new connections. This only works well when the daemon validates passed descriptors, units bound resource usage, security boundaries are explicit, and deployments gracefully drain accepted connections. Start with a small unit pair, test with systemd-socket-activate, and then measure cold-start and restart behavior under real load.




No comments yet. Be the first to share your thoughts.