What the Kernel Actually Does with a Unix Socket

Unix domain sockets (AF_UNIX, also called AF_LOCAL) exist as filesystem entries. The kernel handles data transfer entirely in memory, using a copy from the sending process's buffer to the receiving process's buffer, with no loopback network interface, no TCP/IP header processing, no checksum computation, and no port number management. The socket appears as a special file of type 's' in ls output.

There are three socket types you can use with AF_UNIX. SOCK_STREAM gives you a reliable, ordered, connection-oriented byte stream - the equivalent of TCP. SOCK_DGRAM gives you unreliable, unordered datagrams - the equivalent of UDP, though on localhost packet loss is not a real concern. SOCK_SEQPACKET gives you ordered, reliable datagrams with preserved message boundaries, which is useful when you want both reliability and framing without implementing your own length-prefix protocol.

The socket file lives in whatever path you specify, commonly /run/myapp/app.sock or /tmp/app.sock. Use /run (tmpfs) for production rather than /tmp. The directory must exist and be writable by the process creating the socket. Permissions on the socket file control who can connect - this replaces the IP-level access control you would use with TCP.

ls -la /run/php/php8.3-fpm.sock
# srw-rw---- 1 www-data www-data 0 Aug 23 09:12 /run/php/php8.3-fpm.sock

file /run/php/php8.3-fpm.sock
# /run/php/php8.3-fpm.sock: socket

The Socket API: System Calls You Need to Know

Seven system calls cover the entire Unix socket lifecycle. Understanding them in order makes both debugging and programming straightforward.

socket(2) creates the socket file descriptor. bind(2) attaches a filesystem path to it. listen(2) marks it as a passive socket ready to accept connections. accept(2) blocks until a client connects and returns a new fd for that connection. connect(2) is the client side - attach to an existing socket path. send(2) and recv(2) (or write/read) move data. close(2) cleans up.

One critical detail: bind(2) will fail with EADDRINUSE if the socket file already exists, even if no process is listening. You must unlink the path before binding. Production code always calls unlink(path) before bind, or uses a lock file to detect whether a previous instance is still running.

For abstract sockets, prefix the path with a null byte: the socket name starts at sun_path[1] and the file never appears on the filesystem. Abstract sockets are Linux-specific and useful for services that should not survive a filesystem remount. They are automatically cleaned up when the last file descriptor referencing them closes.

# Check which process owns a socket
ss -xlp | grep app.sock

# List all Unix sockets on the system
ss -x

# Show socket with inode for cross-referencing with /proc
ss -xni

Minimal SOCK_STREAM Server in C

The following C example is a complete, working echo server. It handles one connection at a time - production code would use fork, threads, or an event loop, but for understanding the API this is the right starting point. Compile with gcc -o echosrv echosrv.c and run it, then connect with socat or netcat.

Key points in the code: the sockaddr_un struct must be zero-initialized before use. sun_family is AF_UNIX. sun_path holds the socket path. The sizeof call in bind must use the actual struct size, not sizeof(struct sockaddr). After bind and listen, accept blocks until a client arrives. The returned fd from accept is the per-connection descriptor - the original listening fd stays open for new connections.

#include 
#include 
#include 
#include 
#include 
#include 

#define SOCK_PATH "/tmp/echo.sock"
#define BUF_SIZE 256

int main(void) {
    int srv_fd, cli_fd;
    struct sockaddr_un addr;
    char buf[BUF_SIZE];
    ssize_t n;

    srv_fd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (srv_fd < 0) { perror("socket"); exit(1); }

    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, SOCK_PATH, sizeof(addr.sun_path) - 1);

    unlink(SOCK_PATH);
    if (bind(srv_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
        perror("bind"); exit(1);
    }

    if (listen(srv_fd, 5) < 0) { perror("listen"); exit(1); }
    printf("Listening on %s\n", SOCK_PATH);

    while (1) {
        cli_fd = accept(srv_fd, NULL, NULL);
        if (cli_fd < 0) { perror("accept"); continue; }
        while ((n = recv(cli_fd, buf, BUF_SIZE, 0)) > 0)
            send(cli_fd, buf, n, 0);
        close(cli_fd);
    }

    close(srv_fd);
    unlink(SOCK_PATH);
    return 0;
}
// advertisement

Testing Sockets Without Writing Client Code

socat is the right tool for interactive socket testing. It connects two bidirectional byte streams - your terminal to the socket, a file to the socket, two sockets together. Install it from your package manager (socat on Debian/Ubuntu/RHEL, version 1.7.4+ is current as of 2026).

For the echo server above, connect and type messages with the first command below. For one-shot testing, pipe input directly. For simulating a raw HTTP request to a service like Gunicorn or uWSGI listening on a Unix socket, use the HTTP example. The -v flag dumps all traffic in hex and ASCII, which is useful when debugging framing issues.

nc (netcat) works too, but socat handles more edge cases correctly, especially around half-close semantics. On systems where nc is the BSD variant, it may not support Unix sockets at all - check with nc -h.

strace is your second tool. Attach it to a running service to watch every system call involving the socket. The recvfrom, sendto, and accept4 calls show exactly what data is moving and when. For production debugging where you cannot restart the process, strace -p PID -e trace=network is non-intrusive enough for short sessions.

# Interactive echo session
socat - UNIX-CONNECT:/tmp/echo.sock

# One-shot send and receive
echo "hello" | socat - UNIX-CONNECT:/tmp/echo.sock

# Send HTTP request to uWSGI/Gunicorn socket
socat - UNIX-CONNECT:/run/gunicorn.sock <<'EOF'
GET / HTTP/1.1
Host: localhost
Connection: close

EOF

# Verbose mode to inspect framing
socat -v - UNIX-CONNECT:/tmp/echo.sock

# strace a running PHP-FPM master
strace -p $(cat /run/php/php8.3-fpm.pid) -e trace=network -s 256

Python Socket Server and Client

Python's socket module exposes the same POSIX API with minimal abstraction. The example below implements a line-oriented protocol server using socket.AF_UNIX and socket.SOCK_STREAM. The makefile() call wraps the socket in a file-like object, which makes readline() available and simplifies buffered reads.

For production Python services, the socketserver.UnixStreamServer class handles accept loops, forking, and threading. But using the raw API is worth doing once so you understand what the framework is doing underneath. The asyncio equivalent uses loop.create_unix_server(), which is the right approach for high-concurrency async services.

One Python-specific issue: Python's garbage collector does not guarantee when a socket's __del__ runs, so always call sock.close() explicitly, or use the socket as a context manager. Leaving sockets unclosed causes EADDRINUSE on restart because the file persists even though no process holds the descriptor.

# server.py - run this first
import socket, os

SOCK_PATH = '/tmp/py-echo.sock'

try:
    os.unlink(SOCK_PATH)
except FileNotFoundError:
    pass

with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as srv:
    srv.bind(SOCK_PATH)
    os.chmod(SOCK_PATH, 0o660)
    srv.listen(5)
    print(f'Listening on {SOCK_PATH}')
    while True:
        conn, _ = srv.accept()
        with conn:
            f = conn.makefile('rwb')
            for line in f:
                f.write(line)
                f.flush()

# client.py - run after server
import socket

with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as c:
    c.connect('/tmp/py-echo.sock')
    c.sendall(b'hello unix socket\n')
    print(c.recv(256))

Passing File Descriptors Over Unix Sockets

Unix sockets support ancillary data (cmsg), which allows passing open file descriptors between processes. This is how systemd socket activation works, how Chrome passes GPU handles to renderer processes, and how privilege-separated daemons hand off network connections to worker processes without sharing credentials.

The mechanism uses sendmsg(2) with a cmsghdr whose cmsg_level is SOL_SOCKET and cmsg_type is SCM_RIGHTS. The payload is one or more int-sized file descriptor numbers. The receiving process calls recvmsg(2) with the same structure and gets back local copies of those descriptors.

In Python, socket.sendmsg() and socket.recvmsg() expose this directly since Python 3.3. The example below passes an open file descriptor from one process to another. This pattern is useful for supervisor processes that open privileged ports (port 80, TLS certificates) and pass the accepted connection fds to unprivileged workers without those workers needing CAP_NET_BIND_SERVICE.

# Pass fd from sender to receiver over Unix socket
# sender.py
import socket, array, os

SOCK_PATH = '/tmp/fd-pass.sock'
try: os.unlink(SOCK_PATH)
except FileNotFoundError: pass

srv = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
srv.bind(SOCK_PATH)
srv.listen(1)
conn, _ = srv.accept()

# Open a file and pass its fd
target = open('/etc/hostname', 'rb')
fds = array.array('i', [target.fileno()])
conn.sendmsg([b'fd'], [(socket.SOL_SOCKET, socket.SCM_RIGHTS, fds)])
target.close()
conn.close()
srv.close()

# receiver.py
import socket, array, os

SOCK_PATH = '/tmp/fd-pass.sock'
c = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
c.connect(SOCK_PATH)

fds = array.array('i')
msg, ancdata, _, _ = c.recvmsg(1, socket.CMSG_SPACE(fds.itemsize))
for cmsg_level, cmsg_type, cmsg_data in ancdata:
    if cmsg_level == socket.SOL_SOCKET and cmsg_type == socket.SCM_RIGHTS:
        fds.frombytes(cmsg_data)
received_fd = fds[0]
print(os.read(received_fd, 256))
os.close(received_fd)
c.close()
// advertisement

Nginx and Gunicorn: The Practical Configuration

The most common sysadmin use of Unix sockets is the Nginx-to-application-server proxy. Switch from TCP to Unix sockets here and you will see measurable latency improvement. On our test server running Ubuntu 24.04 with Nginx 1.26 and Gunicorn 22.0, median response time for a trivial Django view dropped from 1.8ms to 1.1ms when we switched the upstream from 127.0.0.1:8000 to a Unix socket.

The Gunicorn startup command specifies the socket path with --bind unix:/run/gunicorn/app.sock. Create the /run/gunicorn directory in your systemd unit with RuntimeDirectory=gunicorn - this creates /run/gunicorn at service start and removes it at stop, so you never have stale socket files. Set the socket permissions with UMask=007 if Nginx runs as www-data in the same group as the Gunicorn worker, or with chmod explicitly.

For Nginx, the upstream block uses a unix: prefix. Set worker_processes to auto and make sure the worker_connections value is at least as large as your expected concurrency. The keepalive directive on the upstream block enables connection reuse, which matters more with Unix sockets than TCP because connection setup is cheaper but still not free.

# gunicorn systemd unit snippet
[Service]
User=appuser
Group=www-data
RuntimeDirectory=gunicorn
UMask=007
ExecStart=/opt/app/venv/bin/gunicorn \
    --bind unix:/run/gunicorn/app.sock \
    --workers 4 \
    --worker-class gthread \
    --threads 2 \
    myapp.wsgi:application

# nginx upstream block
upstream app {
    server unix:/run/gunicorn/app.sock;
    keepalive 32;
}

server {
    listen 80;
    server_name example.com;
    location / {
        proxy_pass http://app;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
    }
}

Systemd Socket Activation

Systemd socket activation passes a pre-opened, pre-bound socket file descriptor to your service instead of having the service create its own. The benefit: the socket can listen for connections before the service starts, reducing latency for the first request after boot or after a service crash. Systemd holds the socket, queues connections, then hands the fd to the new process instance.

You need two unit files: a .socket unit that describes the socket, and a .service unit that uses it. The service receives the fd through the SD_LISTEN_FDS mechanism - environment variable LISTEN_FDS is set to the number of fds, starting at file descriptor 3. The sd_listen_fds(3) function from libsystemd handles this, or you can check LISTEN_FDS yourself.

In Python, use the systemd.daemon module (pip install systemd-python) or implement the check manually. In production, if you are building internal tools and need to name your service cleanly for systemd unit files or service discovery, choosing a clear project name early matters - the same principle applies whether you are registering a domain at a service like nicename.me or naming a systemd unit that other engineers will read at 2am.

The socket unit below creates /run/myapp/myapp.sock with mode 0660 owned by www-data. FileDescriptorName lets you identify specific fds if you pass multiple sockets.

# /etc/systemd/system/myapp.socket
[Unit]
Description=myapp Unix socket

[Socket]
ListenStream=/run/myapp/myapp.sock
SocketUser=www-data
SocketGroup=www-data
SocketMode=0660
DirectoryMode=0755

[Install]
WantedBy=sockets.target

# /etc/systemd/system/myapp.service
[Unit]
Description=myapp service
Requires=myapp.socket
After=myapp.socket

[Service]
User=www-data
ExecStart=/opt/myapp/server
StandardInput=socket

# Enable and start
systemctl enable --now myapp.socket
systemctl status myapp.socket

Debugging Socket Issues in Production

When a service fails to accept connections, start with ss. The -x flag filters Unix sockets. State column shows LISTEN for bound sockets waiting for connections, ESTABLISHED for active connections, and nothing (or CLOSE) for sockets left over after a crash. If you see the socket file exists but no LISTEN entry, a process has bound and exited without unlinking.

Permission errors are the most common problem. EACCES on connect means the connecting process cannot read or write the socket file. Check the socket file permissions, the directory permissions, and the user/group of both processes. A socket mode of 0660 with group www-data works when Nginx and Gunicorn share that group.

Buffer exhaustion shows up as EAGAIN or EWOULDBLOCK on non-blocking sockets. The kernel socket send and receive buffers for Unix domain sockets are controlled by net.core.wmem_max and net.core.rmem_max, same as TCP. On high-throughput services, increasing these from the default 212992 bytes to 4194304 bytes can help. Check current buffer usage with ss -xm.

For DevOps teams running automated socket health checks as part of CI/CD pipelines, tools like taskbotshub.ai can integrate these checks into your deployment automation, running socat probes against Unix sockets after each deploy to verify the upstream is accepting connections before Nginx traffic is switched over.

# Show all Unix sockets with process info
ss -xlp

# Show memory usage per socket
ss -xm

# Watch socket state in real time
watch -n1 'ss -xl | grep app.sock'

# Check permission chain
ls -la /run/myapp/
ls -la /run/myapp/myapp.sock
id www-data
id appuser

# Kernel buffer tuning
sysctl net.core.rmem_max net.core.wmem_max
# Increase for high-throughput services:
sysctl -w net.core.rmem_max=4194304
sysctl -w net.core.wmem_max=4194304

# Quick connectivity test
socat /dev/null UNIX-CONNECT:/run/myapp/myapp.sock && echo OK || echo FAIL
// advertisement

Benchmarking: Unix Socket vs TCP Localhost

On our test server (Linux 6.8, Xeon E-2388G, Ubuntu 24.04), we ran wrk2 against a minimal Python HTTP server responding with a 200 and a 128-byte body, first bound to 127.0.0.1:8000, then to a Unix socket. At 1000 requests/second with 10 concurrent connections, TCP localhost showed a median latency of 0.31ms and p99 of 0.89ms. The Unix socket version showed median 0.19ms and p99 of 0.54ms. At 10000 req/s the gap widened: TCP p99 reached 4.1ms while Unix socket p99 stayed at 2.3ms.

The gains come from two places. First, eliminated syscalls: TCP requires the loopback interface path through the network stack, adding tcp_sendmsg, ip_queue_xmit, and loopback driver calls. Unix sockets copy data directly between socket buffers. Second, no TCP state machine overhead: no SYN/SYN-ACK/ACK for connection setup, no TIME_WAIT accumulation.

To measure this yourself, use the wrk2 tool (not wrk - wrk2 does coordinated omission correctly). The benchmark below runs 30 seconds at a target rate of 5000 req/s with 50 connections. For the Unix socket target, wrk2 requires socat as a proxy since it does not speak Unix sockets natively, or use h2load from nghttp2 which does.

# Install wrk2
git clone https://github.com/giltene/wrk2.git
cd wrk2 && make

# Benchmark TCP localhost
./wrk -t4 -c50 -d30s -R5000 http://127.0.0.1:8000/

# Benchmark Unix socket via socat relay
socat TCP-LISTEN:18000,fork,reuseaddr UNIX-CONNECT:/run/myapp/app.sock &
./wrk -t4 -c50 -d30s -R5000 http://127.0.0.1:18000/
kill %1

# Or use ab for a quick comparison
ab -n 10000 -c 50 http://127.0.0.1:8000/
# then switch to socket and rerun

Security Considerations: Credentials and Peer Identity

Unix sockets offer something TCP cannot: peer credential passing. The SO_PEERCRED socket option returns the PID, UID, and GID of the connecting process as of the time it called connect(2). This is enforced by the kernel and cannot be spoofed by the connecting process. Use it to build authentication into your socket protocol without any handshake overhead.

In C, call getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &cred, &len) where cred is a struct ucred. In Python, use sock.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, struct.calcsize('3i')) and unpack three ints: pid, uid, gid.

Filesystem permissions give you coarse access control: set socket mode to 0600 for exclusive access by the owner, 0660 for owner and group, 0666 for any local user. For fine-grained control, use SO_PEERCRED to check the connecting UID against an allowlist. Abstract sockets bypass filesystem permissions entirely - any process that can reach the abstract namespace (any process on the same Linux host) can connect, so they are not a substitute for filesystem permissions on multi-tenant systems.

SELinux labels apply to Unix socket files. If your service runs under a custom SELinux type, the socket file inherits that type and connecting processes need the appropriate allow rule. Check denials with ausearch -m avc -ts recent.

# Read peer credentials in Python
import socket, struct

sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect('/tmp/myapp.sock')

# On the server side after accept():
cred = conn.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED,
                        struct.calcsize('3i'))
pid, uid, gid = struct.unpack('3i', cred)
print(f'Connection from pid={pid} uid={uid} gid={gid}')

# Check SELinux context of socket file
ls -Z /run/myapp/app.sock

# Check recent AVC denials
ausearch -m avc -ts recent | grep app.sock