The idea: same API, two kinds of address

A program that talks to a server on the same machine, such as psql to PostgreSQL or the docker command to dockerd, has two choices. It can open a TCP connection to 127.0.0.1:5432, the loopback address. Or it can open a unix domain socket (AF_UNIX) at a path such as /var/run/postgresql/.s.PGSQL.5432. The calls are the same, socket(), connect(), send(), recv(); only the address changes. What the kernel does with the bytes is very different.

Left: a message from psql to postgres over TCP goes down the client's socket, TCP and IP, through the lo device, then up IP, TCP and the server's socket: 8 hops. Right: over a unix socket it goes from the client's socket straight onto the server socket's receive queue: 3 hops. Both make 2 copies.
Loopback TCP still runs the whole network stack twice; a unix socket hands the data straight to the peer.

The canvas runs the same client and server both ways on one clock. One tick is one hop of one message from one stop to the next: from the program into its socket, from the socket into TCP, from TCP into IP, and so on. The unix side usually finishes first and waits while the TCP side is still climbing.

What loopback TCP still does

Packets to 127.0.0.1 never touch a network card, but they go through every layer as if they would. send() copies the bytes into an skb (the kernel's packet buffer). TCP adds a header with the sequence and acknowledgment numbers. IP adds its header, looks up the route (dev lo) and runs the netfilter hooks. The loopback device lo hands the packet back with netif_rx(), and it waits for the NET_RX softirq, just like a packet from a real NIC. Then it climbs IP and TCP again on the server side: socket lookup by the 4-tuple, sequence check, an ACK owed, the bytes queued, the server woken. That is 8 hops one way, 16 for a request and its reply (Demo: One query). The checksum is the one thing skipped: lo marks its packets as already checked. The receive half is the same climb as on How Linux Receives a Packet, without the ring and the interrupt.

What a unix socket does instead

A unix stream socket has no headers, no ports, no sequence numbers and no device. The kernel already knows the peer socket, so unix_stream_sendmsg() copies the bytes into an skb and links it straight onto the peer's receive queue, then wakes the peer. recv() copies the bytes out. That is 3 hops one way. The data is still copied twice, user space to kernel and kernel to user space, exactly as with TCP. The saving is the layers in between, not the copies. Ten queries make the difference plain: 160 hops against 60, and 40 copies on each side (Demo: Ten queries). Real benchmarks usually show unix sockets clearly ahead for small requests; for large messages the copying dominates and the gap shrinks.

Connecting: handshake vs path lookup

A TCP connect() is the 3-way handshake: SYN, SYN-ACK, ACK, each going down one stack and up the other, then the new socket waits on the listener's accept queue until the server calls accept(). On the canvas that is 16 ticks before a single byte is sent. A unix connect() looks the path up like any file name, checks write permission on the socket file, finds the listening socket through the inode and queues a connected socket on its accept queue. No packet is sent; connect() returns at once. On the canvas that is 4 ticks (Demo: Connect).

Who may connect: file permissions and SO_PEERCRED

A TCP port has no owner and no permission bits: any process on the machine can connect to 127.0.0.1:2375. A unix socket is a file, so the usual permission check applies. Docker's socket is srw-rw---- root docker: root and members of group docker may connect, nobody else. In Demo: Docker socket, bob (not in the group) gets EACCES on the unix socket after 3 ticks, but the TCP port lets him straight in. Since the Docker API can start a privileged container, that TCP port would make every local user root. This is why dockerd listens on the socket by default and warns against -H tcp:// without TLS.

Two users try to reach dockerd. Through the socket file /var/run/docker.sock, srw-rw---- root docker: alice in group docker is allowed, bob gets EACCES. Through TCP 127.0.0.1:2375: both alice and bob get in, and dockerd only sees 127.0.0.1 and a port number.
The socket file's permission bits are access control; a loopback port lets every local user in.

The server can also ask the kernel who is on the other end: getsockopt(fd, SOL_SOCKET, SO_PEERCRED) returns the peer's pid, uid and gid, recorded at connect() and impossible to fake. PostgreSQL's peer authentication (local all all peer in pg_hba.conf) uses it to let OS user alice in as role alice with no password. Over TCP the server only sees 127.0.0.1:54012, which says nothing about the user, so pg_hba.conf usually asks for a password there (scram-sha-256). How PostgreSQL forks a backend per connection is on PostgreSQL Process Architecture.

Passing a file descriptor with SCM_RIGHTS

A file descriptor is just an index into one process's fd table; the number 4 means nothing to another process. A unix socket can still hand an open file across: sendmsg() with a control message of type SCM_RIGHTS makes the kernel take a reference to the open file itself, and recvmsg() installs it in the receiver as a new descriptor (fd 7 on the canvas). Both now share one open file, offset included (Demo: Pass an open file). On a TCP socket the same call fails with EINVAL. Real uses: systemd hands listening sockets to the services it starts, container runtimes pass a terminal (runc --console-socket), Wayland clients pass graphics buffers, and some servers pass listening sockets to their new version during a reload. More on fds and sockets: A TCP Socket in Linux.

Side by side

TCP on 127.0.0.1Unix domain socket
AddressIP and port, 127.0.0.1:5432file path, /var/run/postgresql/.s.PGSQL.5432
Hops one way (canvas)8: socket, TCP, IP, lo, IP, TCP, socket3: socket, peer's queue
Copies per message22
connect()3-way handshake, 16 tickspath lookup, 4 ticks
Who may connectany local userwho may write the socket file
Who is the peeran IP and a portSO_PEERCRED: pid, uid, gid
Pass open filesno (EINVAL)yes, SCM_RIGHTS
Reach another machineyes, change the addressno, same kernel only

So the rule of thumb: use the unix socket when client and server share a kernel (the default for psql, the Docker CLI, MySQL's localhost, nginx to a local PHP-FPM or gunicorn), and TCP when they might not. Containers are the usual catch: two containers in different network namespaces cannot reach each other's 127.0.0.1, but they can share a unix socket file through a mounted volume, and mounting /var/run/docker.sock into a container hands it full control of Docker (What a Container Really Is, How Docker Networking Works on One Host).

What the page leaves out

  • Real timing: a hop here is one tick, but the TCP and IP hops are not equal in cost to the unix ones, and the wake-up and context switch cost the same on both sides.
  • Delayed ACKs, the receive window, Nagle and TCP_NODELAY, segmentation of large writes, GSO/GRO.
  • Datagram (SOCK_DGRAM) and SOCK_SEQPACKET unix sockets, socketpair(), and abstract socket names (a path starting with a zero byte, no file).
  • SCM_CREDENTIALS, passing several fds at once, and the limits on fds in flight.
  • The PostgreSQL protocol itself (startup message, authentication exchange, RowDescription, DataRow) and the HTTP request docker sends.
  • Faster local paths: shared memory, pipes, and TCP over loopback with zero-copy or io_uring.