open atlas
↑ Back to track
Linux, the operating system LIN · 03 · 04

Socket activation and custom targets

Socket activation lets systemd hold a socket open while the service starts lazily on first connection — enabling zero-downtime restarts and faster boots. Custom targets group related services into a named synchronisation point you can activate, isolate, or depend on.

LIN Senior ◷ 24 min
Level
FoundationsJuniorMiddleSenior

You restart your API service to deploy a new binary. For the two seconds while the new process is starting, every incoming connection gets “connection refused”. A load balancer might retry, but a mobile client probably shows an error. There is a systemd primitive that eliminates this gap entirely: the socket unit. systemd holds the listening socket open across the restart — the kernel queues the connections in its backlog — and passes the socket to the new process the moment it is ready. The client never sees the gap. This is socket activation, and it is built into systemd with no code changes to your service.

Goal

After this lesson you can explain how socket activation works and why it enables zero-downtime restarts, write a paired .socket and .service unit, describe when to use Accept=yes vs Accept=no, and create a custom .target unit to group and synchronise related services.

1

Socket activation separates “holding the socket” from “running the service.” systemd owns the socket; it starts the service on demand. The key insight: the kernel’s TCP backlog can buffer incoming connections even while the process is not yet listening. systemd creates the socket, the kernel accepts connections into its backlog, and when the first connection arrives, systemd starts the service and hands it the file descriptor. The client’s connect() succeeds immediately — it just blocks briefly in the kernel backlog.

A socket unit and its paired service unit share a name (everything before the extension):

sshd.socket   → systemd holds the socket
sshd.service  → systemd starts this when a connection arrives

The socket unit is always enabled; the service unit is started lazily.

2

Writing a socket unit. The minimal shape for a TCP socket on port 8080:

# /etc/systemd/system/myapi.socket
[Unit]
Description=My API socket

[Socket]
ListenStream=8080
# Accept=no (default): one service instance handles all connections
# Accept=yes: systemd forks one service instance per connection (inetd style)

[Install]
WantedBy=sockets.target

ListenStream= is for TCP (stream sockets). For UDP use ListenDatagram=. For Unix domain sockets use a path: ListenStream=/run/myapi.sock.

Accept=no (the default) is almost always what you want: systemd starts one instance of the service, passes it the listening socket via file descriptor 3 (the SD_LISTEN_FDS protocol), and the service handles all connections itself. Accept=yes spawns a new service instance per connection — useful only for very simple per-connection handlers (like the classic inetd model). Most modern services use Accept=no.

3

The paired service receives the socket via SD_LISTEN_FDS. The service does not call bind() or listen() itself — it receives the already-bound, already-listening socket from systemd via an environment variable and inherited file descriptors:

# /etc/systemd/system/myapi.service
[Unit]
Description=My API service
Requires=myapi.socket
After=myapi.socket

[Service]
Type=notify
ExecStart=/opt/myapi/server
# Do NOT specify a port here — the socket is inherited from myapi.socket
Restart=on-failure
User=myapi

[Install]
# Note: no WantedBy here — the socket unit pulls the service in
Also=myapi.socket

The Also= directive in [Install] means “when you enable this service, also enable myapi.socket”. This keeps the pair in sync.

For zero-downtime restarts: systemctl restart myapi.service stops and starts the service while myapi.socket keeps listening. The kernel queues incoming connections during the gap. The new service process starts, calls sd_listen_fds() to get the socket fd, and connections that were queued are served normally.

# Enable both together (Also= handles the socket)
sudo systemctl enable --now myapi.service

# Restart just the service — socket stays listening throughout
sudo systemctl restart myapi.service

# Check that the socket is held open
systemctl status myapi.socket
4

Custom targets group related services and provide a named synchronisation point. A target is just a unit file with .target extension and a [Unit] section — no [Service] section. Its purpose is to be something other units can declare After=, Requires=, or WantedBy= against.

Example: a “backend stack” target that groups your database, cache, and API:

# /etc/systemd/system/backend.target
[Unit]
Description=Full backend stack
Requires=postgresql.service redis.service myapi.service
After=postgresql.service redis.service myapi.service

[Install]
WantedBy=multi-user.target

Now you can:

# Bring up the entire backend stack
sudo systemctl start backend.target

# Stop the entire stack
sudo systemctl stop backend.target

# Isolate to only this target (stops everything else — use with care)
sudo systemctl isolate backend.target

Other units can declare After=backend.target to ensure the full stack is up before they start.

5

PartOf= makes a service stop when its owning target stops, without being a hard dependency. This is the right directive for “member of a group” semantics:

# /etc/systemd/system/myworker.service
[Unit]
Description=Background worker
PartOf=backend.target
After=backend.target

With PartOf=backend.target, if you run systemctl stop backend.target, the worker also stops. But if the worker crashes on its own, backend.target is not affected. This is different from Requires= (which would stop the target if the member crashes). Use PartOf= when units are logically part of a group but their individual failure should not tear down the group.

# Verify the stop propagation
sudo systemctl start backend.target
sudo systemctl stop backend.target
systemctl status myworker.service
# Active: inactive (dead) — stopped because its target stopped
Worked example

Implement zero-downtime restarts for a Go HTTP server using socket activation.

The Go standard library supports socket activation via net.FileListener:

// main.go — receives the socket from systemd instead of binding it
package main

import (
    "net"
    "net/http"
    "os"
)

func main() {
    // SD_LISTEN_FDS: systemd passes sockets starting at fd 3
    f := os.NewFile(3, "socket")
    ln, err := net.FileListener(f)
    if err != nil {
        // Fallback: bind our own socket (for local dev without systemd)
        ln, err = net.Listen("tcp", ":8080")
        if err != nil {
            panic(err)
        }
    }
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("ok"))
    })
    http.Serve(ln, nil)
}

Unit files:

# myapp.socket
[Socket]
ListenStream=8080

[Install]
WantedBy=sockets.target
# myapp.service
[Service]
Type=simple
ExecStart=/opt/myapp/server
Restart=on-failure

Deploy a new binary:

# Copy new binary
sudo cp myapp-v2 /opt/myapp/server

# Restart the service — socket stays listening the whole time
sudo systemctl restart myapp.service

# Confirm: connections during the restart gap were queued, not refused
curl http://localhost:8080/   # should succeed immediately after restart
Why this works

macOS launchd has had socket activation since OS X 10.4 (2005) — it predates systemd’s equivalent by several years. The Sockets key in a launchd plist is equivalent to ListenStream= in a .socket unit. macOS system daemons like sshd use launchd socket activation — that is why ssh connects instantly to a macOS box even though sshd may not have been continuously running. systemd adopted the same model and standardised it via the SD_LISTEN_FDS environment variable and the libsystemd sd_listen_fds() function.

Common mistake

Do not set ListenStream=0.0.0.0:8080 — the 0.0.0.0: prefix is not needed and is ignored in newer systemd versions, which may log a warning. Just use ListenStream=8080 to listen on all interfaces, or ListenStream=127.0.0.1:8080 to restrict to localhost. For Unix domain sockets use an absolute path: ListenStream=/run/myapi/myapi.sock. Also: do not forget Also=myapi.socket in the [Install] section of the service, otherwise systemctl enable myapi.service will not enable the socket unit and the service will never be triggered.

Check yourself
Quiz

You run `systemctl restart myapi.service` while the paired `myapi.socket` is active. What happens to incoming TCP connections during the two seconds the service is restarting?

Recap

Socket activation splits “holding the socket” (.socket unit, always active) from “running the service” (.service unit, started on demand). systemd owns the socket; the kernel queues connections in its backlog during service restarts, giving zero-downtime behaviour with no application-level coordination. The service receives the already-bound socket via fd 3 (SD_LISTEN_FDS). Accept=no (default) is the right choice for services that handle their own connection loop. Custom targets are named synchronisation points — you declare Requires= and After= inside the target, and other units use WantedBy= or PartOf= to join it. PartOf= propagates stop (but not failure) from the target to the member, making it the right directive for group membership without tight failure coupling.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 4 done

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources1
expand
  1. 01

Trademarks belong to their respective owners. Editorial reference only.