Chapter 6: Unix Domain Sockets

Updated

October 31, 2026

Chapter 6: Unix Domain Sockets: Fast Local Communication

When two processes on the same machine need to communicate—such as an Nginx reverse proxy routing requests to a local Go application, or Docker communicating with its daemon—using loopback TCP (127.0.0.1) introduces unnecessary overhead.

Unix Domain Sockets (UDS) provide bidirectional, stream-oriented (AF_UNIX) or datagram-oriented inter-process communication entirely within kernel memory, bypassing TCP/IP framing, checksum calculations, routing tables, and network device drivers.

In this chapter, you will learn how to build Unix domain socket servers and clients in Go, manage socket file permissions and lifecycle cleanup, utilize Linux abstract namespace sockets, and analyze performance gains over TCP loopback.


What Are Unix Domain Sockets and Why Use Them

A Unix Domain Socket looks and behaves like a standard net.Conn, but instead of an IP address and port number, its endpoint is a path on the filesystem (e.g., /var/run/app.sock) or an abstract string in the kernel.

+-------------------------------------------------------------------+
|               Unix Domain Socket vs. Loopback TCP                 |
+-------------------------------------------------------------------+

Loopback TCP (127.0.0.1:8080):
  User Process -> TCP Checksum -> IP Routing -> Loopback Driver (lo) 
               -> Kernel Socket Buffer -> User Process
  (Overhead: TCP handshakes, ACK packets, sequence tracking, windowing)

Unix Domain Socket (/tmp/app.sock):
  User Process -> Kernel Memory Buffer Copy -> User Process
  (Zero network protocol overhead; direct memory transfer)

Advantages of Unix Domain Sockets

  1. Lower Latency & Higher Throughput: No IP headers, no TCP checksums, no packet fragmentation, and no ACK packets.
  2. Standard File Permissions: Security is governed by POSIX file permissions (chmod, chown). You can restrict access to specific users or groups without firewall rules.
  3. Passing File Descriptors: Using SCM_RIGHTS (via golang.org/x/sys/unix), processes can pass open file descriptors, network connections, or shared memory handles directly across a Unix socket.

Creating Unix Domain Socket Servers and Clients

1. The Unix Socket Server

One critical difference when using filesystem-backed Unix sockets: when the server stops, the socket file remains on disk. If the server restarts without deleting the stale file, net.Listen returns bind: address already in use.

// uds_server.go
package main

import (
    "bufio"
    "errors"
    "io"
    "log"
    "net"
    "os"
    "os/signal"
    "syscall"
)

const SocketPath = "/tmp/go_socket_demo.sock"

func main() {
    // Remove stale socket file if it exists
    if err := os.Remove(SocketPath); err != nil && !os.IsNotExist(err) {
        log.Fatalf("failed to remove existing socket: %v", err)
    }

    ln, err := net.Listen("unix", SocketPath)
    if err != nil {
        log.Fatalf("failed to listen on UDS: %v", err)
    }
    defer ln.Close()
    defer os.Remove(SocketPath)

    // Clean up socket file on termination signals
    sigChan := make(chan os.Signal, 1)
    signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
    go func() {
        <-sigChan
        _ = ln.Close()
        _ = os.Remove(SocketPath)
        os.Exit(0)
    }()

    log.Printf("UDS Server listening on %s", SocketPath)

    for {
        conn, err := ln.Accept()
        if err != nil {
            if errors.Is(err, net.ErrClosed) {
                return
            }
            log.Printf("Accept error: %v", err)
            continue
        }

        go handleUDSConn(conn)
    }
}

func handleUDSConn(conn net.Conn) {
    defer conn.Close()

    reader := bufio.NewReader(conn)
    for {
        line, err := reader.ReadBytes('\n')
        if err != nil {
            if err != io.EOF {
                log.Printf("read error: %v", err)
            }
            return
        }

        // Echo back
        if _, err := conn.Write(line); err != nil {
            return
        }
    }
}

2. The Unix Socket Client

Connecting to a Unix socket in Go uses the standard net.Dial API:

// uds_client.go
package main

import (
    "bufio"
    "fmt"
    "log"
    "net"
)

func main() {
    conn, err := net.Dial("unix", "/tmp/go_socket_demo.sock")
    if err != nil {
        log.Fatalf("dial error: %v", err)
    }
    defer conn.Close()

    // Send message
    if _, err := conn.Write([]byte("Hello from Unix Domain Socket client!\n")); err != nil {
        log.Fatal(err)
    }

    reply, err := bufio.NewReader(conn).ReadString('\n')
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Echoed: %s", reply)
}

File Permissions and Security with Unix Sockets

Because Unix domain sockets exist as special files in the filesystem, you can use os.Chmod and os.Chown immediately after calling net.Listen to restrict access:

ln, err := net.Listen("unix", "/var/run/secure_app.sock")
if err != nil {
    log.Fatal(err)
}

// Restrict access: only owner (UID 1000) and members of 'www-data' group can connect
// File mode 0660: rw-rw----
if err := os.Chmod("/var/run/secure_app.sock", 0660); err != nil {
    log.Fatalf("failed to set socket permissions: %v", err)
}

Abstract Namespace Sockets (Linux-specific)

On Linux, Unix domain sockets support an abstract namespace (RFC 2553 / Linux man unix(7)).

Abstract sockets: 1. Do not exist as files in the filesystem directory tree. 2. Are automatically cleaned up by the kernel when all referencing file descriptors close. 3. Never leave stale files behind after a server crash. 4. Are denoted by a null byte (\x00) prefix in the path, or an @ symbol in Go:

// In Go, prefix the address with '@' for an abstract socket:
const AbstractSocketName = "@my_service_abstract_socket"

ln, err := net.Listen("unix", AbstractSocketName)
if err != nil {
    log.Fatalf("failed to bind abstract socket: %v", err)
}
defer ln.Close()

// Client dials the exact same abstract address:
conn, err := net.Dial("unix", AbstractSocketName)

[!NOTE] Abstract namespace sockets are exclusive to Linux. Code relying on them will fail to bind on macOS, FreeBSD, or Windows.


Comparing Performance: Unix Sockets vs Loopback TCP

Under high concurrency and large transfer volumes, Unix domain sockets provide substantial throughput and latency advantages over loopback TCP:

Metric Loopback TCP (127.0.0.1) Unix Domain Socket
P99 Latency (Small RPCs) ~45 µs ~18 µs (2.5x faster)
Max Throughput (Gigabytes/sec) ~3.2 GB/s ~6.8 GB/s (2.1x higher)
CPU Utilization (Context Switches) High (TCP/IP stack processing) Low (direct VFS memory buffer copy)
Socket Cleanup on Crash Automatic (kernel closes TCP port) Manual (stale file) or automatic (abstract)
Ephemeral Port Exhaustion Risk under heavy short-lived connections No port numbers involved

Chapter Summary

  • Unix Domain Sockets allow processes on the same operating system to communicate without TCP/IP stack overhead.
  • Sockets implement the standard net.Conn and net.Listener interfaces.
  • Filesystem-backed sockets require explicit cleanup of stale files upon startup and shutdown.
  • Access can be restricted using standard POSIX file permissions (os.Chmod).
  • On Linux, abstract namespace sockets (@name) provide automatic cleanup without touching the filesystem.
  • In the next chapter, we will master concurrency patterns for network programming in Go.