Plan 9 from Bell Labs: What Rob Pike Built After Unix

plan-9unixdistributed-systemsrob-pikeken-thompsonbell-labsresearchoperating-systems

Rob Pike joined Bell Labs in the early 1980s, walking into the Computing Sciences Research Center — the lab that had already produced Unix and C. By the mid-1980s, the computing world was moving toward networks of personal workstations, and the people who had built Unix could see the problems that approach would create. Each machine administered separately. Hardware obsolete before it’s paid for. Every workstation a compromise — too slow for heavy computation, too expensive for just running a window system.

So they started over.

The argument

Plan 9 is not a product. Rob Pike has described it as “an argument” — an argument for simplicity and clarity, for following ideas to their logical conclusion rather than layering hacks on top of hacks. Dennis Ritchie, who created C and co-created Unix, put it more precisely:

Plan 9 really pushes hard on some ideas that Unix has that haven’t really been fully developed, in particular, the notion that just about everything in the system is accessible through a file.

Unix had the right idea: everything is a file. But Unix didn’t follow through. Networking uses Berkeley sockets — a completely separate API with its own special-purpose syscalls. The X Window System has its own protocol, its own resource model, its own everything. ioctl calls are “completely disgusting hacks,” to quote one Hacker News commenter, and they’re not wrong.

Plan 9 asked: what if we actually meant it?

Two ideas

The designers — Pike, Ken Thompson, Dave Presotto, Phil Winterbottom, and others — built the entire system on two foundations:

Per-process namespaces. Every running process has its own view of the filesystem. /dev/cons always refers to your terminal, but which actual device that is depends on the process. Unlike Unix, where mounting a filesystem is a global operation requiring root privileges, Plan 9 lets any process customize its own namespace without affecting anyone else.

A message-oriented file protocol. Everything communicates through 9P, a protocol with about 14 message types that handles all file operations — open, read, write, close. That’s it. No special APIs for networking, no separate protocol for windowing, no ioctl escape hatch. If you want to offer a service, you implement a file server that speaks 9P.

From these two ideas, everything else falls out.

How it works in practice

Consider networking. On Unix, creating a TCP connection means calling the socket syscall to create a magic file descriptor, then connect to establish a connection. These are special-purpose operations with no relationship to the filesystem.

On Plan 9, you open /net/tcp/clone to reserve a connection. You read the connection ID from it. You open /net/tcp/n/ctl and write connect 127.0.0.1!80 to it. Now you open /net/tcp/n/data — that’s your full-duplex stream. No magic syscalls. You could do the whole thing in a shell script.

Now consider what happens when you combine this with per-process namespaces and 9P’s network transparency. Want a VPN? Mount a remote machine’s /net/ether0 at /net/ether1 in your namespace. That’s it. Want remote audio? Mount the other machine’s /dev/audio. Want to debug a process running on a different machine with a different CPU architecture? Import its /proc tree and run a local debugger against it.

There’s a story from the Plan 9 community about a developer working on speech synthesis. He went to give a demo and realized his PC didn’t have a working sound card. So he imported /dev/audio from the next machine over. None of his software knew or cared that the audio was remote. Before the demo, he paused and said: “Can we just take a moment to appreciate how cool it is that this just works?”

The window system trick

The window system, originally called 8½ and later Rio, illustrates the elegance. Plan 9 represents user interface devices through three pseudo-files: mouse (for mouse events), cons (for text I/O), and bitblt (for graphics operations). When the window system creates a new window, it sets up a new namespace where mouse, cons, and bitblt point to itself instead of the real hardware. Programs don’t know if they’re talking to actual devices or to a window manager. They don’t need to know.

This means Rio is simultaneously a window manager and a VNC server. Mount your local /dev/draw and /dev/kbd on a remote machine, and you’ve got remote desktop. No special protocol. No X11-style complexity. The same mechanism that makes windows work also makes remote windows work.

Containers before containers

Here’s where it gets remarkable. Since everything is truly a file, and per-process namespaces don’t require special permissions, making a container is trivial: unmount the hardware you don’t want the sandboxed process to have access to. Done. You don’t need to be root.

Want to restrict a process to specific TCP ports? Write a hundred lines of shell script that implements a limited /net/tcp, and mount it into the namespace.

This predates BSD jails by several years. It predates Docker by two decades. And it’s implemented more cleanly than either, because it’s not a special-purpose mechanism — it’s just the natural consequence of per-process namespaces and “everything is a file” being actually true.

What leaked out

Plan 9 failed commercially. Rob Pike has been characteristically direct about why:

I used Plan 9 as my local operating system for a year or so after joining Google, but it was just too inconvenient to live on a machine without a C++ compiler, without good NFS and SSH support, and especially without a web browser.

Or as Eric Raymond put it: “The most dangerous enemy of a better solution is an existing codebase that is just good enough.”

But the ideas didn’t die. They leaked into everything:

UTF-8 — Pike and Thompson invented it on a diner placemat in 1992 for Plan 9. It’s now used by over 97% of websites.

/proc filesystems — FreeBSD’s /proc is modeled directly on Plan 9’s. Linux’s /proc and /sys follow the same principle.

Container namespaces — Linux’s clone() syscall is modeled on Plan 9’s rfork(). Per-process mount namespaces in Linux are Plan 9’s central idea, implemented twenty years later. Docker and Kubernetes are built on top of these.

Go — Pike, Thompson, and Robert Griesemer created Go at Google in 2007. Its goroutines descend from Plan 9’s concurrent programming experiments (Newsqueak, Alef, Limbo). Its composition-over-inheritance philosophy comes directly from Plan 9, where all system data items implemented exactly the same 14-method file system API. Pike has said Docker was Go’s “killer app” — and Docker’s entire purpose is container namespaces, which are Plan 9’s core idea.

The lineage is direct: Plan 9 → Linux namespaces → Docker (written in Go, a Plan 9 descendant) → Kubernetes (also Go). The infrastructure running most of the internet’s services is built from Plan 9 ideas, implemented in a Plan 9 descendant language.

The 2012 lament

In a Uses This interview from 2012, Pike said something that still stings:

When I was on Plan 9, everything was connected and uniform. Now everything isn’t connected, just connected to the cloud, which isn’t the same thing. And uniform? Far from it, except in mediocrity. This is 2012 and we’re still stitching together little microcomputers with HTTPS and ssh and calling it revolutionary. I sorely miss the unified system view of the world we had at Bell Labs, and the way things are going that seems unlikely to come back any time soon.

He’s right, and we should sit with that for a moment. We have a more powerful version of the fragmented workstation model that Plan 9 was explicitly designed to replace. We have containers that approximate Plan 9 namespaces through layers of kernel complexity. We have microservices communicating over HTTP that could be filesystem operations. We have service meshes that are elaborate reimplementations of “mount a remote service into your namespace.”

We got the pieces. We didn’t get the coherence.

Why this matters now

I’m building an agent system that stitches together processes across machines — Redis event bridges, Inngest durable functions, gateway daemons, Kubernetes pods. Every integration is a special-purpose protocol. Every connection is bespoke plumbing.

Plan 9 says this is the wrong approach. The right approach is a uniform interface — everything looks like a file, everything speaks one protocol, and composition happens through namespace manipulation rather than API integration.

We’re not going back to Plan 9. But understanding what it got right — and what we lost by not following through on its ideas — is essential context for anyone building distributed systems today.

The dream of “mount the remote service into your namespace and just use it” hasn’t been replaced by anything better. It’s been replaced by a lot of things that are worse, individually and collectively.

Further reading

Research suggested by Sean Grove.