Unix sockets and fd inheritance¶
Two ways to bind that aren't a plain TCP port — AF_UNIX for
local reverse proxies, and fd inheritance for systemd socket
activation.
AF_UNIX — local domain socket¶
When BlackBull runs behind a local reverse proxy (nginx, Caddy),
using an AF_UNIX socket eliminates TCP overhead and avoids
exposing a port on 0.0.0.0:
# CLI
blackbull myapp:app --bind unix:/run/blackbull.sock
# nginx upstream
upstream blackbull { server unix:/run/blackbull.sock; }
# In Python
app.run(unix_path='/run/blackbull.sock')
The socket file is created with mode 0660 so a reverse proxy
running in the same group can connect without a chmod. A
leftover socket file at the path is removed automatically
before bind; BlackBull refuses to unlink a regular file or
directory at that path (safety check).
TCP-only socket options (SO_REUSEPORT, TCP_USER_TIMEOUT,
IPV6_V6ONLY) are skipped for AF_UNIX sockets — they carry
no meaning on a domain socket.
fd inheritance — systemd socket activation¶
Systemd can pre-bind the port as root and then start BlackBull unprivileged, handing the bound socket as an open file descriptor:
[Socket]
ListenStream = 443
[Install]
WantedBy = sockets.target
[Service]
User = www-data
ExecStart = blackbull myapp:app --bind fd://3
# In Python — equivalent shape
app.run(inherited_fd=3)
BlackBull validates the $LISTEN_PID / $LISTEN_FDS
environment variables per the sd_listen_fds(3) protocol:
LISTEN_PIDmust equal the current process PID — if it points elsewhere BlackBull refuses to adopt the fd (prevents accidentally stealing another process's socket).LISTEN_FDSdefines the valid fd window[3, 3 + LISTEN_FDS). Fds outside that window are rejected.
When neither variable is set (non-systemd handoff, tests) BlackBull accepts the fd unconditionally.
With BB_SOCKET_REUSEPORT=1, several workers and no
--reload, a creator-held IPv4-only, IPv6-only, or dual-stack
socket would leave some or all connections unserved — startup refuses each
shape instead (see
Several workers behind an activated
socket).
What systemd activation buys you¶
- Bind privileged ports without running as root. systemd
binds
:443while running as root; BlackBull starts aswww-dataand inherits the already-bound socket. - Zero-downtime restarts. systemd keeps the socket open across stop/start cycles — connections that arrive while the new BlackBull is launching queue in the kernel accept buffer instead of being refused.
- Lazy activation. The socket is ready before BlackBull starts; the first connection wakes the service.
Several workers behind an activated socket¶
systemd-socket-activate execs the service in place, so the port is
the workers' to re-bind; a systemd.socket unit keeps its own copy for
the service's whole lifetime, and ReusePort=yes leaves that copy in the
port's reuseport group — the per-worker bind then succeeds while the
supervisor's copy keeps taking connections no worker accepts on. A socket
bound in another network namespace refuses for the same reason: the workers
would bind where its clients are not. The namespace comparison needs
SO_NETNS_COOKIE (Linux 5.14+); a host that will not give it reads the
socket as local, and that refusal is skipped silently. Where
/proc/net/tcp* cannot be read — or reads empty — a released
ReusePort=yes socket is refused too: there is nothing to tell it apart
from a kept one.
The startup window¶
Accepts begin only after lifespan startup completes, so arrivals before then
wait in the kernel's accept queue, bounded by the creator's Backlog= where
the fd is adopted and by
BB_SOCKET_BACKLOG where BlackBull
binds it. The window lasts as long as the startup hook runs, with no deadline
from the framework. Budget startup memory across every listener's backlog
and the receive buffers of parked connections. Effective kernel values are
platform-dependent; adopted sockets retain the creator's backlog.
BB_SOCKET_RCVBUF affects listeners BlackBull binds and is inherited by their
accepted connections. Long startup hooks can exceed clients' connect budgets.
What a client beyond the backlog observes¶
Until startup completes, backlog + 1 connections can wait. Beyond that:
| family | the client |
|---|---|
| TCP | retransmits, and is served once startup completes |
AF_UNIX, non-blocking (nginx, asyncio, a Python socket with a timeout) |
is refused at once (EAGAIN) |
AF_UNIX, blocking |
waits, and connects once the queue drains |
Set BB_SOCKET_BACKLOG above the number of connections you expect during
startup, especially on AF_UNIX. An adopted fd (--bind fd://N) keeps the
backlog it was created with until accepting opens, and BB_SOCKET_BACKLOG
after: size the startup burst where the socket is created, such as systemd's
Backlog=. net.core.somaxconn caps both. On shutdown an activated socket's
file stays in place; a stale file left after --reload is replaced on the next
start.
On Linux, a worker that finds an AF_UNIX listener's queue full as it opens
accepting logs one socket_backlog warning on blackbull.caps, naming the
listener, the connections waiting and the backlog. An adopted fd created in
another network namespace (systemd PrivateNetwork=yes) cannot be read and is
not checked.
Inspecting the bind¶
server.port returns the kernel-assigned port number after
open_socket(port=0) — useful in tests where you want an
ephemeral port:
from blackbull.server import ASGIServer
server = ASGIServer(app)
server.open_socket(port=0)
print(server.port) # e.g. 39423
For AF_UNIX sockets server.port is the path string instead
of a port number.
Next¶
- Behind a reverse proxy — pairing
AF_UNIXwith an upstream reverse proxy. - Running BlackBull — the broader entry-point overview.
- Configuration —
--bindsyntax for the CLI (host:port,unix:/path,fd://N).