The Life of a Unix Socket

ByLinuxtastic

September 3, 2026

A socket-activated service restarts and its clients start getting “no such file or directory”. systemctl status app.socket says active (listening); ss -xl and lsof both print the path. The path does not exist. The kernel stored that string at bind() and every tool is quoting it back, unchecked, while the file it named was deleted when the service’s runtime directory was cleaned up. That failure is the shape of this whole subject: a Unix socket has a name in a directory and a copy of that name inside itself, and nothing keeps the two together.

Almost every service on your machine is listening on a Unix socket right now: dbus, /dev/log, docker, systemctl, probably your database. The Life of a Packet follows the family that goes through the network stack; this page is about the one that never does, and almost everything the packet article teaches about sockets is false for this family. The queue columns it tells you to read can be fabricated. The receive buffer it tells you to tune does nothing. The full listen queue it says will refuse a connection will block it instead. What a Unix socket has instead is a name with an owner and a mode, a peer identity the kernel fills in, and the ability to carry an open file descriptor to another process, and each of those three is wrong in a specific, reproducible way that every diagnostic tool hides. Descriptor passing itself is The Life of a File Descriptor‘s; namespaces are Containers, All the Way Down‘s.

The seven stages of a Unix socket. Every section below is named after one of them.

  1. Create — socket() makes an object with no name at all.
  2. Name — bind() does two separate things, and this is where the trouble starts.
  3. Listen — a queue that blocks rather than refuses.
  4. Be handed one — most processes did not create the socket they use.
  5. Connect and accept — one permission bit, and a peer identity that never admits it does not know.
  6. Send — message boundaries, the sender’s buffer, and a queue ten items deep.
  7. Close — and what becomes of the name.

The diagnostic hinge: two things carry the name of a Unix socket, and neither of them checks the other.

bind() creates an entry in a directory — a name, with an owner and a mode, that anything able to write that directory can delete or replace. It also writes a copy of that string into the socket itself, where it stays forever, unchecked, and where ss, lsof and /proc/net/unix read it back to you. Nothing keeps the two together after bind() returns. A socket can go on running under a name that no longer exists, and a name can come to point at a completely different socket, and every tool on the machine will show you both as healthy.

So do not read the name back off a tool. Try to connect, and read the error.

nc -U /run/foo.sock </dev/null ; echo $?
socat - UNIX-CONNECT:/run/foo.sock          # names the errno more precisely
What it saysWhat it meansStages
ENOENTThere is no name. The socket may still be running.2–3
ECONNREFUSEDThe name exists and nothing is behind it — a file left behind by something that stopped, or, for an abstract socket, the wrong network namespace.2, 7
EACCESThe name exists and something is behind it. This is a permission question and nothing else.2
EAGAIN, or it hangsThe listener is there and is not accepting.3–4
It connectsThe binding is sound. Every remaining problem is yours.6–7

Those five errnos are the kernel’s documented semantics rather than local state, which is why they can be tabulated at all when most of what follows cannot be.

The exception, and it is a real one: a socket you did not name has nothing to test. A socketpair(), a descriptor that arrived over a socket, and a listener handed to you by systemd all have no path you can connect to. For those the answer is always stages 6–7 — and stage 4 is where you find out what you were actually given.

Stage 1: Create

socket(AF_UNIX, SOCK_STREAM, 0) returns a descriptor to an object with no name, no peer and no address, and it is fully usable in that state: socketpair() hands you two already joined. The object has an inode in a filesystem called sockfs that has no mount point and that you can never look at, and that produces a first-class trap. fchmod() on a socket descriptor returns 0 and changes nothing anyone will ever consult. It writes to the sockfs inode; the mode that governs access is on the filesystem inode bind() created, and only chmod() on the path reaches it:

fstat(fd)   : ino=2901     mode=140777 dev=9        # sockfs, invisible, never checked
stat(path)  : ino=950608   mode=140777 dev=65024    # the inode connect() checks

fchmod(fd, 0700) = 0 ; now:
  fstat(fd) mode=140700   stat(path) mode=140777    # the path did NOT change

A call that succeeds is not a call that did what you meant, the same shape as fcntl(F_SETFL, O_CLOEXEC) in The Life of a File Descriptor. Restrict a socket with umask or chmod on the path, or better on the directory, which the next stage covers.

Stage 2: Name

There are four kinds of name. A pathname socket has an entry in a directory: /run/foo.sock. An abstract socket has a name in a per-network-namespace table and no filesystem presence; tooling writes it @name, and its address begins with a NUL byte. An unnamed socket has no name and works. And a socket autobinds to an abstract name of five hex digits when you bind it with an address length of just the family field, or when SO_PASSCRED is set on an unbound socket and it then sends or connects:

fresh DGRAM socket                            -> UNNAMED
after setsockopt(SO_PASSCRED, 1)              -> UNNAMED
after sendto() with SO_PASSCRED on            -> ABSTRACT @6365c
a plain DGRAM socket after sendto()           -> UNNAMED

The name appears at the first send or connect, not at setsockopt, so an auditor grepping for stray abstract names sees them later than expected. Two mechanical limits: abstract names are not C strings, so "\0my\0secret" prints as @my in most tooling; and sun_path is 108 bytes, so binding a 199-character path succeeds, having silently bound a 108-character one.

The write bit, and only the write bit

A pathname socket’s mode is checked when a client connects, and the bit that matters is write. Read and execute are irrelevant:

socket mode 0444 : connect() -> Permission denied
socket mode 0222 : connect() -> OK
socket mode 0644 : connect() -> Permission denied
socket mode 0666 : connect() -> OK
socket mode 0755 : connect() -> Permission denied
socket mode 0777 : connect() -> OK

The corpus has this backwards in both directions. A socket created under the default umask of 022 comes out 0755, which nobody but its owner can connect to. And chmod 644 on a socket to make it safe breaks it for everyone but the owner, while looking reasonable in a listing; so do 0640 and 0775. The directory is the better control, and it needs only search permission, so 0711 confines and also hides the name:

socket 0777, directory 0755          : connect() -> OK
socket 0777, directory 0700          : connect() -> Permission denied
socket 0777, directory 0711 (x, no r): connect() -> OK

The manual says connecting to a stream socket requires write permission and says nothing about datagrams. The same check applies to sendto() on a datagram socket: 0444 refuses, 0222 works, and /dev/log is a datagram socket. Under systemd the mode is probably not yours anyway: SocketMode= defaults to 0666, so a socket from an unmodified .socket unit is connectable by every local user.

The abstract namespace

An abstract socket has no inode, so no owner and no mode; fchmod() on one returns 0 and writes a mode nothing consults. Its access control is exactly one thing, the network namespace:

same netns, connect as root:     connect to @lt-abstract: OK
same netns, connect as uid 1000: connect to @lt-abstract: OK
a NEW network namespace:         connect to @lt-abstract: Connection refused
a new MOUNT namespace only:      connect to @lt-abstract: OK

A wrong network namespace gives ECONNREFUSED, not ENOENT, so it presents as “the service is down” rather than “I cannot see it from here”; a unit with PrivateNetwork=yes produces exactly this, and so does every container. And a container run with --net=host shares the host’s abstract namespace, so any process in it can connect to anything named there as any uid. That is CVE-2020-15257 (10 December 2020): containerd’s shim API was on an abstract socket whose only check was that the peer’s uid matched its own. containerd moved the shim to pathname sockets in 1.3.9 and 1.4.3. If you are choosing, choose a pathname socket in a directory you control.

“No permissions at all” stopped being strictly true on 17 November 2024: Landlock ABI 6, in Linux 6.12, added LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET, restricting a sandboxed process to abstract sockets created within its own Landlock domain. Kernel documentation describes a further right for pathname sockets under ABI 9; landlock(7), revised 21 April 2026, stops at ABI 8. Worth knowing about, not yet worth building on.

Stage 3: Listen

listen() takes a backlog, and it behaves the opposite way to TCP’s. A full AF_UNIX backlog blocks a connection; it does not refuse it. A non-blocking connect() gets EAGAIN; a blocking one waits until somebody calls accept():

backlog was 5, and nothing ever called accept():
  6 connects succeeded
  connect #7 failed: Resource temporarily unavailable   (non-blocking)
  BLOCKING connect on a full backlog: returned 0 after 2s, when a slot freed

So a service with a stuck accept loop does not present as clients being refused. It presents as clients hanging: no retry logic fires, no circuit breaker trips, nothing logs an error. And Recv-Q on the listening socket, which for TCP counts connections waiting to be accepted, reads 0. Stage 6 explains why.

Stage 4: Be handed one

On a current machine most processes did not create the socket they use. systemd creates it and passes it to the service; a supervisor creates it and forks; a privileged helper sends it to a worker over another socket. This is where a socket’s identity becomes uncertain, and almost every silent failure in this subject lives here. Socket activation is the common case: systemd binds and listens, and the service inherits the descriptor through the environment:

LISTEN_PID=2107      # I am 2107
LISTEN_FDS=1
LISTEN_FDNAMES=mysock
  fd 3 -> socket:[4237]

Descriptors start at 3 (SD_LISTEN_FDS_START); FileDescriptorName= defaults to the socket unit’s name with its suffix; and LISTEN_PID must equal getpid(). That last rule is behind a very quiet failure: a daemon that forks into the background before calling sd_listen_fds() sees a mismatch, gets 0, and falls through to creating its own socket. Nothing errors. The unit reports success. Which brings us to the worst idiom in the subject, present in nearly every “write a Unix socket server” tutorial:

# the universal tutorial idiom
unlink("/run/app.sock");
bind(fd, ...);
listen(fd, 5);

Under socket activation that code orphans the listener systemd handed you. LISTEN_FDS=1 arrives, is ignored, and the socket unit goes on reporting active (listening) on a socket nobody will ever accept from. It fails by producing two working-looking listeners rather than an error; the worked diagnosis at the end is exactly this.

A socket that arrived over another socket is the same situation from the receiver’s side: you hold something you did not create and cannot assume anything about. The descriptor does not announce its type, whether it is listening, or how many you got; ask with SO_TYPE, SO_ACCEPTCONN and getsockname(), which is why sd_listen_fds() and sd_is_socket_unix() exist. From a shell:

sudo ls -l /proc/$(systemctl show -P MainPID app.service)/fd
systemctl show app.socket -p Listen -p SocketMode -p RemoveOnStop
systemctl show app.service -p RuntimeDirectory -p RuntimeDirectoryPreserve

Stage 5: Connect and accept

The permission check from stage 2 happens here, on the socket’s mode and on every directory in the path. What happens after the connection exists is what makes Unix sockets worth having and is the largest single source of security bugs in local services: the kernel will tell you who is on the other end. A server reads SO_PEERCRED on the accepted socket and gets a pid, uid and gid recorded at connect() time, impossible for the peer to forge. No other local transport offers that. As normally used, it is also a mistake.

SO_PEERCRED has no error return. Every way it can fail to know the answer, it returns a number instead.

There is no errno meaning “I cannot tell you who this is”. On a socket that has never been connected it returns success and fills the structure with sentinel values that look like data:

SO_PEERCRED on a brand-new, unconnected STREAM socket: rc=0 pid=0 uid=-1 gid=-1

Across a PID namespace the pid comes back as 0 — not an error, and not -1, but a number that is a perfectly valid argument to kill(2) meaning “every process in my process group”. Across a user namespace that does not map the peer, the uid comes back as the overflow uid, 65534, and two different unmapped peers are indistinguishable:

server in a PID namespace, client is real pid 2282:
  SO_PEERCRED: pid=0 uid=0 gid=0

server in a user namespace with uid_map "0 1000 1", client is real uid 0:
  SO_PEERCRED: pid=2316 uid=65534 gid=65534

The values are translated into your namespaces, and when the translation cannot be performed it is not reported as an error — it is reported as a plausible number. A service that checks cred.uid == 0 for admin operations sees 65534 for actual root once it is packaged into a rootless container, and nothing in its configuration changed.

If you need an answer that can be wrong loudly, ask for SO_PEERPIDFD, added in Linux 6.5 on 27 August 2023. It returns a reference to the peer process rather than a number, so when the peer is gone it says so and cannot silently become a different process.

The namespace cases surprise people, but the commonest failure is this:

--- (1) a client connecting DIRECTLY ---
  client pid is 2142
  backend sees SO_PEERCRED pid=2142 (python3) uid=0 gid=0

--- (2) the SAME client through systemd-socket-proxyd ---
  client pid is 2147
  backend sees SO_PEERCRED pid=2146 (systemd-socket-) uid=0 gid=0

Put anything in the path, systemd-socket-proxyd, socat, an nginx or Envoy upstream, a service-mesh sidecar, and the credentials are the proxy’s. The kernel’s statement is true; it answers a different question from “who is asking me to do this”. The number also decays: the peer can exec after connecting, so the pid stays valid and the program behind it changes, and the peer can exit and have its pid recycled while your connection is still open:

connection accepted; SO_PEERCRED pid=2165 uid=0
the client has exited and been reaped; the connection is still open
burning pids until 2165 is handed out again...
  after 3763 forks, pid 2165 was reissued to a brand new process
  SO_PEERCRED on the old connection still says pid=2165 uid=0

SCM_CREDENTIALS is checked, not filled: the peer states its own pid, uid and gid, and the kernel rejects a claim it is not entitled to make. An unprivileged process cannot lie; root can, claiming pid=1 uid=0 with the send succeeding. It is strictly weaker than SO_PEERCRED, and it is the only option on a datagram server, which has no accept() and so no SO_PEERCRED at all; /dev/log is one. And the client can read SO_PEERCRED too, getting the server’s credentials as they were at listen(): a check that it is talking to a root-owned daemon rather than an impostor socket dropped into a writable directory.

Stage 6: Send

SOCK_STREAM is a byte stream with no message boundaries. Both SOCK_DGRAM and SOCK_SEQPACKET preserve boundaries, though most tutorials credit only the second, and both set MSG_TRUNC and discard the remainder on a short read:

wrote 3+5+1 bytes, then read once | 10-byte message read 4 at a time
  SOCK_STREAM      9 bytes: AAABBBBBC | got 4 (TRUNC=no),  next read 6
  SOCK_DGRAM       3 bytes: AAA       | got 4 (TRUNC=yes), next read -1
  SOCK_SEQPACKET   3 bytes: AAA       | got 4 (TRUNC=yes), next read -1

What is distinctive about SOCK_SEQPACKET is that it has a connection and therefore an end of file, and that is its one footgun: a zero-length message is indistinguishable from EOF. A read returning 0 might be a message; read again, and EAGAIN means the peer is alive, a second 0 means it is gone.

Flow control is the sender’s, and the queue is ten

For a stream socket the amount you can write before blocking is governed by the sender’s SO_SNDBUF. The receiver’s SO_RCVBUF does nothing, the exact inverse of TCP:

sender SO_SNDBUF=212992  receiver SO_RCVBUF=212992
  wrote 180224 bytes before EAGAIN
receiver RCVBUF forced to 8192:
  sender still wrote 180224 bytes
sender SNDBUF forced to 8192:
  sender wrote 8128 bytes

For a datagram socket the limit is not bytes at all. It is a count of ten datagrams regardless of size, set by /proc/sys/net/unix/max_dgram_qlen; eleven fit, because the check is a strict comparison, and SO_RCVBUF is irrelevant:

payload 1024 B  rcvbuf=212992  -> 11 datagrams queued
payload   64 B  rcvbuf=212992  -> 11 datagrams queued
payload    1 B  rcvbuf=212992  -> 11 datagrams queued
payload 1024 B  rcvbuf=  8192  -> 11 datagrams queued

Unix datagram sockets do not drop messages; they make the sender stop. A log collector that stalls for a moment stops the service that is writing to it, with no error anywhere, just a process that has become intermittently slow. And raising the sysctl changes nothing, because the value is latched into the socket at creation time:

receiver created BEFORE sysctl raised to 200:  11 datagrams queued
receiver created AFTER  sysctl raised to 200: 201 datagrams queued

Raise it, then restart the receiver, in that order. A connected pair, including anything from socketpair(), is memory-limited instead. unix(7) does not mention max_dgram_qlen; what it says is that Unix datagram sockets are reliable and do not reorder, which is true and, alone, tells the reader there is nothing to worry about.

Receiving descriptors: the two-descriptor bug

Sending an open file descriptor to another process is the one thing a Unix socket can do that nothing else can; what crosses is a reference to the open file description, and The Life of a File Descriptor covers that. The receiving side is where the bugs are:

sender: sent 3 descriptors in one SCM_RIGHTS message
receiver: budgeted for one, with CMSG_SPACE(sizeof(int))
receiver: open descriptors before recvmsg = 6
receiver: recvmsg returned 1, msg_flags=0x8  MSG_CTRUNC=YES
receiver: cmsg_len says 2 descriptor(s) actually arrived
receiver: open descriptors after recvmsg  = 8

Three descriptors were sent and two arrived; the kernel destroyed the third. CMSG_SPACE(sizeof(int)) has room for two descriptors, because of alignment, so code that budgets for one and reads one receives two and leaks the second, which is why the bug never appears in a test that passes a single descriptor. The surplus is closed by the kernel, so the sender’s sendmsg() succeeds and neither end learns anything went missing. And the peer controls the count: send 200 descriptors to a receiver that budgets for one and it leaks one per message, unbounded, from a peer that needed only permission to connect.

Correct receive code loops over CMSG_FIRSTHDR and CMSG_NXTHDR, takes the count from cmsg_len, and checks msg_flags & MSG_CTRUNC. Since Linux 6.16 (merged 23 May 2025) a receiver can refuse descriptors outright with SO_PASSRIGHTS, and the sender fails with EPERM; it defaults to enabled, so you must ask. The reason it exists: an untrusted peer can send you a descriptor pointing at a hung NFS mount, and you will block forever inside close().

ss -x is not ss -t. Its queue columns can be fabricated, with exit status 0.

The Life of a Packet teaches you to read Recv-Q and Send-Q, and that advice is sound for TCP. ss gets those numbers from a netlink interface; for Unix sockets the provider is a kernel module called unix_diag. When it is unavailable, ss silently falls back to parsing /proc/net/unix, which carries no queue lengths and no peer information. It does not warn. It prints 0 rather than “unknown”, * for every peer, SYN-SENT for pending connections, and exits 0:

180224 bytes are sitting unread on this ESTABLISHED socket,
and 10 more connections are unaccepted on the listener.

u_str SYN-SENT 0      0      /tmp/gate/q3.sock 0     * 0
  ... (ten of these)
u_str ESTAB    0      0      /tmp/gate/q3.sock 4133  * 0

$ ss -x >/dev/null 2>&1 ; echo $?
0

Distribution kernels build it as a module, so on an ordinary host it autoloads and the numbers are real. Inside a container without CAP_SYS_MODULE, if the host has not already loaded it, it cannot be loaded — so the place you are most likely to be debugging is the place the numbers are most likely to be invented. Check before you believe them:

lsmod | grep -w unix_diag

# the decisive test: queue something, then look for it
python3 -c '
import socket
a, b = socket.socketpair(); a.setblocking(False); n = 0
try:
  while True: n += a.send(b"x" * 4096)
except BlockingIOError: pass
print("queued", n, "bytes"); input("now run: ss -xp | grep python3")'

If ss reports Recv-Q 0 against that, its queue columns are fabricated on this machine and every capacity conclusion drawn from them is invented.

A word about speed

The figure everybody quotes, that Unix sockets are two to three times faster than loopback TCP, traces to a single Node.js benchmark from March 2014 that did not set TCP_NODELAY; the reason usually given, that TCP has a checksum to compute, has never been true on loopback. On one 2-vCPU virtual machine running Linux 6.18, with TCP_NODELAY set and three pinned runs, round trips were within noise at 1 and 64 bytes and loopback TCP was 10–28% faster at 64 KB. That is one machine too; the point is that the ratio is not a property of the mechanism. The reason to use a Unix socket is a name with an owner and a mode, a peer the kernel will identify, and no address reachable from off the machine.

Stage 7: Close

Closing the last descriptor destroys the socket. It does not remove the name, and neither does systemd: RemoveOnStop= defaults to off, so a left-behind socket file is the normal state of affairs. The file is not inert. It blocks a fresh bind() while correctly telling a client there is nothing behind it:

after close(), the socket file is still there: yes
  a fresh bind() to it: Address already in use     # EADDRINUSE, with nothing listening
  and connect() to it:  Connection refused         # this is how you tell stale from live

stat tells you a file exists. Only connect() tells you whether anything is behind it. EADDRINUSE does not mean something is listening, and killing whatever fuser points at is how an innocent process dies. Deleting the file yourself does not close existing connections and does not merely prevent new ones. It frees the name, and anything that can write that directory can then take it:

UNLINKING /tmp/gate/u.sock now
  the existing connection still reads and writes: fine
  a NEW connect() to the unlinked path: No such file or directory
  a SECOND listener bind()ing the same path: OK -- it now owns the name
  a new client connects; who accepts?
    old listener accept = -1 (EAGAIN)   new listener accept = 9 (got it)

Two LISTEN rows, one path, and nothing on the machine will tell you which one clients reach. That is the hinge stated as a failure.

A worked diagnosis: the socket unit that is listening and unreachable

Back to the socket unit from the top of the page: socket-activated for a year, “no such file or directory” after a routine restart, active (listening) in systemctl status, and a restart of the socket unit that fixes it for a while. Every obvious observable says nothing is wrong:

$ ss -xl | grep app.sock
u_str LISTEN 0  0  /run/lt/app.sock 5769  * 0

$ lsof -U | grep app.sock
systemd-s 2406 root 3u unix 0x…c06f 0t0 5769 /run/lt/app.sock type=STREAM (LISTEN)

/proc/net/unix agrees with both. All three are wrong; two commands tell the truth:

$ ls -l /run/lt/app.sock
ls: cannot access '/run/lt/app.sock': No such file or directory

$ nc -U /run/lt/app.sock </dev/null ; echo $?
Ncat: No such file or directory.
1                                    # ENOENT -- the hinge sends you to stages 2-3

Two documented defaults meeting each other. The .socket unit has ListenStream=/run/lt/app.sock. The .service unit has RuntimeDirectory=lt, and RuntimeDirectoryPreserve= defaults to no, so systemd deletes /run/lt, and the socket file inside it, every time the service stops. The socket unit is not watching, because RemoveOnStop= defaults to off, so it stays active (listening) forever on a name that no longer resolves, and ss, lsof and /proc/net/unix keep printing the path the kernel stored at bind().

The nastier variant is the service also running the tutorial idiom from stage 4, unlink and rebind. Now the hinge’s connect branch says everything is fine, because something answers:

daemon: systemd handed me 1 listening fd(s) starting at 3
daemon: unlinked the stale socket file
daemon: bound and listening on /tmp/gate/svc.sock myself (inode 950586)

$ ss -xl | grep svc.sock
u_str LISTEN 0  0  /tmp/gate/svc.sock 4511  * 0
u_str LISTEN 0  0  /tmp/gate/svc.sock 5691  * 0

One path, one process, two listening sockets, and systemd’s is the one that will never accept again, so the service is never re-activated and nothing logs a word. The fix is one line in the unit (RuntimeDirectoryPreserve=yes, or move the socket out of the runtime directory) and one deletion in the daemon. Your tools do not resolve the name; they repeat it.

What the manual does not say

unix(7) and socket(7) were both revised on 8 February 2026, and neither mentions SO_PEERPIDFD, SO_PASSPIDFD, SCM_PIDFD or SO_PASSRIGHTS: the options that fix the two most dangerous properties of a Unix socket, a peer identity that goes stale and descriptors you cannot refuse. The subject has moved a great deal in three years:

ChangeKernelDate
SO_PEERPIDFD, SO_PASSPIDFD, SCM_PIDFD6.527 Aug 2023
The AF_UNIX garbage collector was replaced outright6.1014 Jul 2024
Landlock can scope abstract sockets (ABI 6)6.1217 Nov 2024
SO_PASSRIGHTS — a receiver can refuse descriptors6.16merged 23 May 2025
user.* extended attributes on sockets, abstract ones included7.114 Jun 2026

The last row is the neatest current fact in the subject: an abstract socket, which has no filesystem presence and no permissions, can now carry metadata, because systemd-journald wants it on /dev/log for protocol negotiation. It gained an attribute surface without gaining a permission surface. And the manual’s paragraph on descriptor truncation never says that MSG_CTRUNC is set, that you should check it, or that the count must come from cmsg_len; a reader who follows it literally writes the vulnerable code.

Symptoms and which stage owns them

What you seeStageWhat to run
ss -xl shows the socket and clients get “no such file”2ls -l the path; the tools print the name from bind()
Socket unit is active (listening) and nothing can reach it2, 4systemctl show app.service -p RuntimeDirectoryPreserve
Permission denied connecting, and the mode “looks fine”2check the write bit; 0644 and 0755 refuse everyone but the owner
Connection refused and the file is definitely there2, 7a stale file; connect() is the only test, not stat
Clients hang instead of being refused3the accept loop is stuck; a full backlog blocks
Half the traffic reaches a process you thought you stopped7ss -xl | grep -F the path; count the rows
The service never gets socket-activated again4it unlinked and rebound; delete that code
Admin checks pass or fail wrongly after containerising5SO_PEERCRED: uid 65534 or pid 0; use SO_PEERPIDFD
Credentials are always the same pid, whoever connects5something is proxying; the kernel is answering about the proxy
A service is intermittently slow and drops nothing6a datagram queue of ten; raise the sysctl, then restart the receiver
Raising SO_RCVBUF changed nothing6stream flow control is the sender’s SO_SNDBUF
Descriptor count climbing on a server that receives them6CMSG_SPACE(sizeof(int)) fits two; check MSG_CTRUNC
ss -x shows nothing queued and the service is clearly stalled—lsmod | grep -w unix_diag; the zeros may be invented

Related reading