ZetCode

C# Unix domain sockets

last modified October 17, 2026

In this article we show how to use Unix domain sockets in C#.

A Unix domain socket is an endpoint for communication between processes on the same computer. Instead of an IP address and a port, it is identified by a path in the file system, such as /var/run/docker.sock. The data never leaves the computer and does not pass through the network stack, so Unix domain sockets are faster than TCP over the loopback interface. Access is controlled by the file system permissions of the socket file.

In .NET, a Unix domain socket is a regular Socket created with the AddressFamily.Unix address family; its address is a UnixDomainSocketEndPoint. Despite the name, Unix domain sockets are also supported on Windows 10 and later.

C# Unix domain socket echo

In the first example, we create a server and a client that communicate through a Unix domain socket.

Program.cs
using System.Net.Sockets;
using System.Text;

string path = Path.Combine(Path.GetTempPath(), "echo.sock");
File.Delete(path);

var endpoint = new UnixDomainSocketEndPoint(path);

using var server = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified);
server.Bind(endpoint);
server.Listen();

Console.WriteLine($"Listening on {path}");

Task serverTask = Task.Run(async () =>
{
    using Socket conn = await server.AcceptAsync();

    var buffer = new byte[1024];
    int n = await conn.ReceiveAsync(buffer);

    string msg = Encoding.UTF8.GetString(buffer, 0, n);
    Console.WriteLine($"Server received: {msg}");

    await conn.SendAsync(Encoding.UTF8.GetBytes(msg.ToUpperInvariant()));
});

using (var client = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified))
{
    await client.ConnectAsync(endpoint);
    await client.SendAsync(Encoding.UTF8.GetBytes("hello from the client"));

    var buffer = new byte[1024];
    int n = await client.ReceiveAsync(buffer);

    Console.WriteLine($"Client received: {Encoding.UTF8.GetString(buffer, 0, n)}");
}

await serverTask;
File.Delete(path);

The client sends a message; the server sends it back in uppercase.

string path = Path.Combine(Path.GetTempPath(), "echo.sock");
File.Delete(path);

var endpoint = new UnixDomainSocketEndPoint(path);

The socket is identified by a path in the temporary directory. If a socket file is left over from a previous run, binding would fail, so we delete it first. The File.Delete method does nothing if the file does not exist.

using var server = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified);
server.Bind(endpoint);
server.Listen();

We create a stream socket in the Unix address family. Binding creates the socket file; Listen starts listening for connections. The rest is the same as with TCP sockets.

using Socket conn = await server.AcceptAsync();

The server task accepts a connection, receives a message, and sends a reply.

await client.ConnectAsync(endpoint);
await client.SendAsync(Encoding.UTF8.GetBytes("hello from the client"));

The client connects to the same endpoint and sends a message.

$ dotnet run
Listening on /tmp/echo.sock
Server received: hello from the client
Client received: HELLO FROM THE CLIENT

C# HttpClient over a Unix domain socket

Some services provide an HTTP API on a Unix domain socket. A well-known example is the Docker Engine, which listens on /var/run/docker.sock. The SocketsHttpHandler has a ConnectCallback property, with which we can tell HttpClient how to open its connections. In the following example, we use it to call the Docker Engine API.

Program.cs
using System.Net.Http.Json;
using System.Net.Sockets;

var handler = new SocketsHttpHandler
{
    ConnectCallback = async (context, token) =>
    {
        var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified);

        try
        {
            await socket.ConnectAsync(new UnixDomainSocketEndPoint("/var/run/docker.sock"), token);
            return new NetworkStream(socket, ownsSocket: true);
        }
        catch
        {
            socket.Dispose();
            throw;
        }
    }
};

using var client = new HttpClient(handler)
{
    BaseAddress = new Uri("http://docker/")
};

var version = await client.GetFromJsonAsync<DockerVersion>("version");
Console.WriteLine($"Docker {version?.Version}, API {version?.ApiVersion}, {version?.Os}/{version?.Arch}");

var images = await client.GetFromJsonAsync<List<DockerImage>>("images/json");

var tagged = (images ?? []).Where(image => image.RepoTags is { Count: > 0 });

foreach (var image in tagged)
{
    Console.WriteLine($"{image.RepoTags![0],-40} {image.Size / 1024 / 1024,5} MB");
}

record DockerVersion(string Version, string ApiVersion, string Os, string Arch);
record DockerImage(string Id, List<string>? RepoTags, long Size);

The program prints the Docker version and the list of local images. To run it, Docker must be installed and the user must have access to the socket; on Linux, the user must be a member of the docker group.

ConnectCallback = async (context, token) =>
{
    var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified);

    try
    {
        await socket.ConnectAsync(new UnixDomainSocketEndPoint("/var/run/docker.sock"), token);
        return new NetworkStream(socket, ownsSocket: true);
    }
    catch
    {
        socket.Dispose();
        throw;
    }
}

The callback is invoked whenever the handler needs a new connection. Instead of connecting to the host and port from the URL, we connect to the Docker socket and return a stream for it. With ownsSocket: true, disposing the stream also closes the socket. If the connection fails, we dispose the socket and rethrow the exception.

using var client = new HttpClient(handler)
{
    BaseAddress = new Uri("http://docker/")
};

The host name in the URL is not used to connect, but HTTP requires one in the Host header, so we choose an arbitrary name.

var version = await client.GetFromJsonAsync<DockerVersion>("version");

The GetFromJsonAsync method sends a GET request and deserializes the JSON response into a record. We declare only the properties we need; the other properties of the response are ignored. The property names are matched case-insensitively.

$ dotnet run
Docker 29.1.3, API 1.52, linux/amd64
mcr.microsoft.com/dotnet/sdk:10.0          875 MB
mcr.microsoft.com/dotnet/aspnet:10.0       219 MB
mcr.microsoft.com/dotnet/runtime:10.0      193 MB
mysql:8.4                                  772 MB
python:3.14-bookworm                       977 MB

Note that the example must be run as a project created with dotnet new console. File-based apps run with dotnet run app.cs have Native AOT enabled by default, which turns off the reflection-based JSON serialization used here.

Source

UnixDomainSocketEndPoint class - language reference

Docker Engine API

In this article we have used Unix domain sockets in C#.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all C# tutorials.