skip to content

In a systemd .socket unit, what changes when you set Accept=yes instead of the default Accept=no, and what must the paired service unit look like in each case?

level: middleimportance: should knowfreq 35%

answer

  1. who calls accept, systemd or the daemon
  2. one long-lived process versus one per client
  3. the @ in the unit name is the tell
  4. inetd, rebuilt in unit files
  5. a process per connection has a price

basics

~20 s

With Accept=no systemd passes the listening socket to one long-lived service that accepts connections itself. With Accept=yes systemd accepts each connection and spawns a fresh instance of a template unit per connection, passing only that one connection — the classic inetd model.

solid answer

~40 s

`Accept=no` is the default and the one you almost always want. systemd hands the *listening* descriptor to a single service instance, which does its own `accept()` loop and handles every client. `Accept=yes` is inetd-style: systemd performs the `accept()` itself, and for each connection starts a new instance of a template unit — `foo.socket` then requires `[email protected]`, not `foo.service` — passing only the accepted connection, typically wired to the process's standard input and output via `StandardInput=socket`. That means one process per connection, with all the fork-and-exec and unit-startup cost that implies, so it is only sensible for trivial or very low-rate services. `MaxConnections=` bounds how many instances may exist at once. Datagram sockets cannot use `Accept=yes` at all, since there are no connections to accept.

code

ini · 16 lines
ini
# /etc/systemd/system/echo.socket
[Socket]
ListenStream=7777
Accept=yes
MaxConnections=16

[Install]
WantedBy=sockets.target

# /etc/systemd/system/[email protected]
[Unit]
Description=Echo handler for one connection

[Service]
ExecStart=/usr/bin/cat
StandardInput=socket

go deeper

for a junior

Remember that Accept=no is the default and means one long-running service handles all connections, while Accept=yes starts a separate process for each connection. Recognising the @ in a template unit name is a good tell.

for a middle

Explain who performs the accept() in each mode, that Accept=yes requires a template unit and typically StandardInput=socket, and why per-connection process startup makes it unsuitable for busy services.

for a senior

Show the operational consequences: instance churn in the journal, MaxConnections= and MaxConnectionsPerSource= as the only admission control, and the cost of process startup for a runtime-heavy service. Say when you would still choose it for a legacy line protocol.

for a principal

Frame it as a concurrency-model decision that belongs to the application, not the init system. Per-connection processes buy isolation and simplicity at a throughput cost, and choosing them at the unit level hides that decision from the people who own the service.

## Two different activation models The `Accept=` setting picks between two genuinely different designs that happen to share a unit-file syntax. ## Accept=no — one daemon, the listening socket This is the default and the modern model. systemd opens the listening socket, and on the first connection starts the service **once**, handing it the listening descriptor as fd 3. From then on the daemon runs its own `accept()` loop and serves everything itself; systemd is not involved again until the service stops. The unit pairing is the plain one: `myapp.socket` activates `myapp.service`. Everything people usually mean by socket activation — on-demand start, boot parallelisation, restarts that do not refuse connections — comes from this mode. Concurrency is entirely the daemon's business: threads, an event loop, a worker pool, whatever it already uses. ## Accept=yes — one process per connection With `Accept=yes` systemd keeps the listening socket for itself and calls `accept()` on every incoming connection. For each accepted connection it instantiates a **template** unit and passes it just that connection. So the units look like this: ```ini # /etc/systemd/system/echo.socket [Socket] ListenStream=7777 Accept=yes [Install] WantedBy=sockets.target ``` ```ini # /etc/systemd/system/[email protected] [Unit] Description=Echo handler for one connection [Service] ExecStart=/usr/bin/cat StandardInput=socket ``` The `@` in the service name is what makes it a template; systemd starts one instance per connection. `StandardInput=socket` connects the accepted socket to the process's stdin (and, by default, its stdout), which is why a program as simple as `cat` becomes a working echo service. This is deliberately the old inetd contract: a program that reads stdin and writes stdout needs no socket code at all. Note what the service does **not** get here: it never sees the listening socket, so it cannot accept anything else, and when it exits only that one connection ends. ## Why Accept=no is the default answer Cost. Every connection under `Accept=yes` means a unit instantiation, a fork, an exec, and a fresh process's worth of startup — for anything with a runtime to initialise, a config file to parse or a database pool to open, that is orders of magnitude more expensive than an `accept()` call in an existing loop. It also fragments observability: hundreds of short-lived unit instances instead of one unit you can watch. The systemd documentation itself recommends `Accept=no` for all but the most trivial services, and the shipped examples that use `Accept=yes` are things like small diagnostic or legacy line-protocol services. ## The guards that come with Accept=yes Because a process per connection is a denial-of-service invitation, the socket unit has limits for it: - `MaxConnections=` — how many instances may run at once (a modest default; connections beyond it are refused). - `MaxConnectionsPerSource=` — the same cap per originating IP address, which is the one that stops a single client from consuming the whole allowance. Under `Accept=no` these do not apply in the same way, because there is only ever one service instance and the daemon does its own admission control. ## Constraints worth remembering `Accept=yes` requires a connection-oriented socket. A datagram socket has no accept step, so it must use `Accept=no`, and the service reads datagrams from the inherited descriptor directly. And a service unit written for one mode cannot be used in the other unchanged: the `Accept=no` daemon expects to `accept()` on what it is given, while the `Accept=yes` handler expects an already-connected socket. Getting this backwards is a common cause of a service that starts, reads nothing, and exits immediately. ## What to say in an interview Describe both models, name the template-unit requirement for `Accept=yes`, and then say clearly that you would default to `Accept=no` for any real service and reserve `Accept=yes` for trivial or legacy line protocols where a process per connection is genuinely cheap.

  • Why does the service unit for Accept=yes need an @ in its name?
    Because systemd starts one *instance* per connection, and instances only exist for template units. `[email protected]` is the template; systemd creates `echo@<instance>.service` for each accepted connection, and each one is an independent unit with its own lifecycle and log entries. A plain `echo.service` could only be started once, which is exactly what the per-connection model needs to avoid.
  • What stops Accept=yes from being used as a denial-of-service amplifier?
    `MaxConnections=` caps how many instances may exist at once, and connections beyond it are refused rather than queued indefinitely. `MaxConnectionsPerSource=` applies the same cap per originating address, which is the more useful of the two, since without it one client can consume the whole allowance. Neither is a substitute for a real rate limit in front of a service that matters.
  • Can you use Accept=yes on a UDP socket declared with ListenDatagram=?
    No. `Accept=yes` is defined in terms of accepting connections, and a datagram socket has none — there is nothing to accept and nothing per-connection to hand to an instance. Datagram sockets always run with `Accept=no`, and the service reads messages from the inherited descriptor itself. If you need per-message isolation, that has to be the application's own design.

saying these in an interview costs you the question

  • Thinks Accept=yes just means the socket is enabled
  • Pairs Accept=yes with a plain non-template service unit
  • Recommends Accept=yes for a high-throughput HTTP service
  • Believes the per-connection instance also gets the listening socket
  • Expects Accept=yes to work on a UDP socket

context