systemd Socket Activation: Starting Services Only When Needed
AI generated
$
/etc
Linux
systemd Socket Activation
Starting services only when needed

Not every background service has to be running the instant a machine boots, some are only needed sporadically and spend most of their time just occupying memory for nothing. Socket activation flips the classic startup model on its head: systemd opens the network socket at boot, but only starts the actual service behind it once the first connection actually arrives. For custom PHP background services in a Magento infrastructure, that is a simple path to a faster boot and lower resource usage, without giving up availability.

10 min read Linux systemd Socket Activation

1. How socket activation works

The basic principle of socket activation is not a new invention by systemd, it is a modern take on a concept that already existed in the classic inetd daemon of early Unix systems: instead of every service opening its own network socket and listening on it permanently, a central instance, in this case systemd itself, takes over opening and holding the socket open.

When an incoming connection arrives on that socket, systemd checks whether the associated service is already running. If it is not, systemd starts the service at that exact moment and hands it the already open socket as a prepared file descriptor, instead of the service having to create and bind a new socket itself. For the requesting client, this entire process is invisible, the connection is merely handled with a fraction of a second's delay.

2. .socket unit files: basic structure

A socket unit defines what systemd should listen on, through the ListenStream= directive for TCP or Unix domain sockets, or ListenDatagram= for UDP. The value can be a plain port number, a combination of address and port, or an absolute path for a local Unix socket, which is often the more performant choice over TCP on loopback for communication between processes on the same machine.

The [Install] section with WantedBy=sockets.target ensures the socket unit is actually enabled at boot. The distinction matters: it is the socket that gets activated, not the service itself, which stays deliberately inactive at this point until the first connection arrives.


# /etc/systemd/system/php-worker.socket
[Unit]
Description=Socket for the PHP background service

[Socket]
ListenStream=127.0.0.1:9501
Accept=no

[Install]
WantedBy=sockets.target

3. Linking a socket unit to a service unit

By default systemd automatically links a socket unit to a service unit of the same name, as long as the file names match apart from the extension, so php-worker.socket pairs with php-worker.service. This implicit naming convention already covers the vast majority of use cases without needing an explicit link.

For cases where a different service name is desired, or where several sockets should activate the same service, the Sockets= directive in the service unit allows an explicit mapping. This flexibility is especially useful when a service needs to be reachable both through a TCP socket for external requests and a Unix socket for internal communication.


# /etc/systemd/system/php-worker.service
[Unit]
Description=PHP background service for image conversion
Requires=php-worker.socket

[Service]
ExecStart=/usr/bin/php /opt/mironsoft/php-worker/worker.php
Sockets=php-worker.socket
StandardOutput=journal

4. Benefits: faster boot and lazy start

The most obvious benefit of socket activation is a faster boot: instead of waiting for every single service to fully initialize during system startup, systemd merely opens the associated sockets, a step that runs practically without delay. The actual, potentially slow startup of the application itself shifts to the moment of first real use.

Another, often underrated benefit is implicit parallelization: because multiple services only need to open their sockets at boot instead of fully initializing, significantly more services can start at the same time, without dependency chains artificially stretching out the boot. A service that talks to another through its socket no longer has to wait until that other service is fully up, only until its socket becomes available.

5. Accept=yes versus Accept=no

The Accept= directive in the socket unit governs a fundamentally different behavior. With Accept=no, the recommended mode for most modern applications, systemd hands the listening socket itself to a single, permanently running instance of the service, which then manages multiple simultaneous connections internally, typically through its own event loop.

With Accept=yes, systemd instead starts a brand new instance of the service for every single incoming connection, similar to the classic inetd behavior. That works for simple, stateless scripts, but causes considerable process startup overhead under a high connection rate, which is why this mode is usually unsuitable for applications with meaningful throughput and better reserved for rare, simple requests.

6. Practical example: a custom PHP background service with socket activation

A typical use case is a PHP background service that handles image conversion for product images outside the regular Magento request cycle, but is only called rarely and therefore does not need to sit permanently in memory. With socket activation, the service stays inactive until a conversion request actually arrives on the socket, so neither memory nor a PHP interpreter process is permanently tied up.

For PHP applications, the sockets extension together with a small helper function is needed to correctly take over the file descriptor handed over by systemd, instead of opening a new socket itself. Once the worker has successfully wrapped the passed in socket, it behaves like a normal, permanently running server for any subsequent connections.


#!/usr/bin/env php
<?php
declare(strict_types=1);

// worker.php: PHP worker started by systemd via socket activation
// The first file descriptor handed over by systemd starts at 3
$fd = 3;
$socket = socket_import_stream(fopen("php://fd/{$fd}", "r+"));

while (true) {
    $client = socket_accept($socket);
    if ($client === false) {
        continue;
    }
    // Conversion logic for incoming requests
    socket_write($client, "OK\n");
    socket_close($client);
}

7. Handling file descriptors: LISTEN_FDS and LISTEN_PID

For a process started via socket activation to recognize that it should take over an already open socket instead of opening a new one itself, systemd sets two environment variables: LISTEN_FDS gives the number of passed file descriptors, LISTEN_PID holds the expected process ID, so that forked child processes do not accidentally misinterpret the variables as meant for themselves.

Languages with systemd bindings, for example through the libsystemd library in C or corresponding wrappers in Go and Python, usually wrap this logic in a function called sd_listen_fds, which validates the passed file descriptors and returns them as usable socket handles. No official library function exists for PHP, which is why taking over the socket, as shown in the example above, has to be done manually through the standard sockets extension.

8. Socket activation with Docker and containers

Inside a Docker container, classic socket activation is barely usable in a meaningful way, because containers usually do not run their own systemd as PID 1, and the container runtime model assumes permanently running processes anyway, not on demand startup. A container that gets terminated once no active process remains would directly contradict the lazy start idea behind classic socket activation.

Socket activation still makes sense on the host system itself, outside containers, for example for custom helper services running alongside a Docker based Magento infrastructure. Anyone wanting to replicate the lazy start idea inside a container environment usually ends up with Kubernetes native mechanisms such as scale to zero instead, which are conceptually similar but technically independent of systemd.

9. Debugging and troubleshooting socket activation

The command systemctl list-sockets shows every active socket unit together with its associated service and current activation state, a good first stop to check whether an expected socket is even open. In addition, systemctl status php-worker.socket shows details about the socket itself, while systemctl status php-worker.service reveals the actual state of the service behind it.

If the service does not start as expected on the first connection, journalctl -u php-worker.service provides the relevant error messages. A common stumbling block is a mismatched naming convention between the socket and service files, or a forgotten Sockets= directive when file names differ, which leads systemd to open the socket correctly but find no matching service once a connection arrives.


# Show every socket unit and its activation state
systemctl list-sockets

# Check the logs of the underlying service after startup problems
journalctl -u php-worker.service -e
Mode Instances per Connection Typical Use Overhead
Accept=no One permanent instance for all connections Applications with their own event loop Low, a single process start overall
Accept=yes A new instance per connection Simple, stateless scripts High under many connections
ListenStream (TCP) Depends on the accept mode Network wide reachability Low to medium
ListenStream (Unix socket) Depends on the accept mode Local inter process communication Very low

Mironsoft

Server administration, Docker hosts, and performance tuning

Linux servers nobody on the team really understands anymore?

We handle setup, hardening, and performance tuning of Linux servers and Docker hosts for Magento deployments, documented and traceable instead of grown and unclear.

Server Audit

Review the existing server configuration for security gaps and performance bottlenecks.

Docker Host Setup

Set up and secure production-ready Docker environments for Magento cleanly.

Monitoring & Tuning

Measure resource usage and tune systemd, kernel, and services with purpose.

10. Summary

Socket Activation

Basic principle

systemd opens the socket, the service only starts on demand

Core command

systemctl list-sockets for an overview of active sockets

Recommended mode

Accept=no for applications with their own event loop

Biggest benefit

Faster boot through parallel socket opening without full startup

11. FAQ: Socket Activation

1What is the historical origin of socket activation?
The concept comes from the classic inetd daemon of early Unix systems, which also opened sockets centrally and started services only on an incoming connection. systemd adopts this principle in a modern form and integrates it deeply into unit management.
2How does systemd know which service belongs to which socket?
By default through an identical naming convention, such as php-worker.socket and php-worker.service. Different mappings can be set explicitly through the Sockets= directive in the service unit.
3What is the difference between Accept=yes and Accept=no?
With Accept=no, a single, permanent instance of the service runs and manages multiple connections itself. With Accept=yes, systemd starts a completely new instance for every single connection, causing significant overhead under a high connection rate.
4How does a PHP application take over the socket passed by systemd?
Through the standard sockets extension and the LISTEN_FDS environment variable. The first passed file descriptor starts at value 3 and can be converted into a usable socket handle with socket_import_stream.
5Does socket activation work inside a Docker container?
Barely in any meaningful way, since containers usually do not run their own systemd as PID 1. Socket activation is better suited to helper services on the host system, while container based environments usually rely on their own mechanisms such as Kubernetes scale to zero for lazy start.
6Why does the boot process go faster with socket activation?
Because systemd only opens the sockets at boot, which happens practically without delay, instead of waiting for every single service to fully initialize. The actual startup shifts to the moment of first use.
7How do I find out whether a socket is active but its service has not started yet?
systemctl list-sockets shows the activation state of every socket unit. A separate systemctl status call for the service unit additionally shows whether the underlying service is already running or still inactive.
8Can a service be activated by multiple sockets at once?
Yes, through multiple entries in the Sockets= directive of the service unit. This is useful when a service needs to be reachable both over TCP for external requests and through a Unix socket for internal communication.
9What happens to incoming connections while the service is still starting?
systemd buffers the connection in the kernel backlog of the already open socket until the service has fully started and begins handling it itself. For the client this only means a minimal delay, not a connection error.
10Where do I find error messages if the service fails to start after the first connection?
journalctl -u .service shows the logs of the service. Common causes of a failed start are a mismatched naming convention between socket and service, or an incorrect Sockets= mapping.