Skip to content
Go back

Why I Chose Unix Sockets for Browser-to-Desktop Communication

4 min read

Four ways to connect a browser extension to a desktop app. Three have obvious trade-offs. The fourth requires writing a native binary — and that’s the one I picked.

Here’s why I chose Native Messaging with Unix sockets, and when you should (or shouldn’t) do the same.

Four Approaches, One Winner

When I built Think, a personal AI assistant with a browser extension that talks to an Electron app + FastAPI backend, I evaluated four approaches:

ApproachLatencyPush EventsComplexityDependencies
HTTP polling from extensionHigh (~100ms+)No (fake it with polling)LowNone
Local WebSocket serverMedium (~5ms)YesMediumWS library
Native Messaging + HTTPMedium (~5ms)NoMediumHTTP in host
Native Messaging + Unix socketsLow (~0.1ms)YesHigherNone

I needed push notifications (app → extension) and low latency for a chatty protocol. That ruled out HTTP polling and Native Messaging + HTTP. WebSockets would work, but Native Messaging with Unix sockets gave me both requirements with zero runtime dependencies.

The Architecture

Extension <—> Browser <—> Native Host (C stub)
                                   |
                          Unix Socket (length-prefixed)
                                   |
                               FastAPI
                                   |
                               Electron

Three layers, each with a purpose:

Native Host: A tiny binary that the browser spawns. On macOS/Linux, it’s ~200 lines of C with no dependencies. It connects to a Unix socket and forwards bytes in both directions.

Unix Socket: Persistent connection between the stub and FastAPI. Uses the same length-prefixed protocol as Native Messaging itself (4 bytes little-endian length, then JSON), so the stub is pure byte forwarding.

FastAPI + Electron: The actual application logic. FastAPI handles the socket server and routes requests. Electron connects via WebSocket for the UI layer.

One gotcha that cost me an afternoon: the socket must exist before the stub tries to connect. If FastAPI isn’t running yet, the stub dies silently and the browser just… stops sending messages. No error, no logs, nothing.

Why Not Just HTTP?

If the native host made HTTP requests to FastAPI instead:

HTTPUnix Socket
Latency~1-5ms per request~0.1ms
Push eventsNo (need polling)Yes
ConnectionNew per requestPersistent
DependenciesNoneNone

The persistent connection is the key win. FastAPI can push events to the extension without the extension asking — “your download finished,” “new data available,” “user clicked something in the app.”

The Trick That Makes It Simple

The native host doesn’t parse anything. Since both Native Messaging and our socket use the same length-prefixed protocol, the stub just moves bytes:

/* Core loop: stdin -> socket -> stdout */
while (1) {
    fread(&msg_len, 4, 1, stdin);      // Length from browser
    fread(buf, 1, msg_len, stdin);     // Message from browser
    send(sock, &msg_len, 4, 0);        // Forward to socket
    send(sock, buf, msg_len, 0);

    recv(sock, &msg_len, 4, 0);        // Response from socket
    recv(sock, buf, msg_len, 0);
    fwrite(&msg_len, 4, 1, stdout);    // Forward to browser
    fwrite(buf, 1, msg_len, stdout);
    fflush(stdout);
}

No JSON parsing, no HTTP overhead, no runtime dependencies. The complexity lives in FastAPI where it belongs.

This took me embarrassingly long to figure out. My first version had the stub parsing JSON, validating messages, handling errors — 600 lines of C that broke constantly. Then I realized: if both sides speak the same protocol, the middle doesn’t need to understand it.

The Windows Tax

macOS/Linux: Unix sockets + C stub. No runtime dependencies, compiles to a tiny binary.

Windows: Named Pipes + Python stub. Windows doesn’t have Unix sockets, and the Named Pipe API requires Win32 calls — easier to use Python with pywin32 than wrestle with C. (Yes, I tried the C route first. Two days of CreateNamedPipe debugging later, I admitted defeat.)

Both platforms use the same length-prefixed protocol, so the FastAPI side stays identical.

When This Is (and Isn’t) Worth It

This architecture makes sense when you need persistent, bidirectional communication — push notifications, chatty protocols, sub-millisecond latency. The trade-off is complexity: native binaries, socket servers, manifest registration across platforms. Expect gotchas — I’ve mentioned the startup ordering and Windows quirks, but there are more. Manifest registration differs across browsers. Chrome and Firefox have different JSON schemas. Debugging is brutal because there’s no console, just silent failures.

If your extension just needs occasional request/response, skip this and use HTTP through the native host. But once you need that persistent channel, sockets are the way.


Full implementation: Think on GitHub


Share this post on: