Why tmux Is a Server and Not a Terminal Program

tmux runs a daemon that owns the ptys and a set of thin clients that connect over a UNIX socket. The client hands its terminal file descriptors to the server, and the server writes the composited screen directly into them.

TL;DR: tmux is a daemon that owns real ptys plus thin clients that connect over a UNIX socket. The client hands its terminal file descriptors to the server, so the server writes the composited screen straight into your terminal — the client isn’t even in the output path.

tmux is not a terminal emulator that draws a picture of a terminal. It’s a daemon that owns real ptys, plus a set of thin clients that connect to it over a UNIX socket and ask it questions.

That split is the single most important architectural decision in tmux, and almost everything else follows from it: why panes survive a disconnect, why several people can watch the same session, why the output goes to the terminal from the server rather than from the client, and why control mode exists at all.

The shape of the system

text
   tmux new-window  ──┐
   tmux attach       ─┤
   tmux -C           ─┼──► client process ──socket──► server process
   your editor       ─┘        │                       │
                                 │                       ├── pty A (vim)
                                 │                       ├── pty B (shell)
                                 └── terminal fd ───────►├── pty C (top)
                                     (passed over the socket)

The client is a short-lived or long-lived process that:

  • connects to the socket,
  • sends its terminal name, terminfo capabilities, cwd, environment and features,
  • passes its stdin and stdout file descriptors to the server,
  • then either reads commands from the socket and writes them to the terminal (the control client), or just waits (the attach client).

The server is the process that owns the ptys, parses their output, maintains the grid, and writes the composited screen into the client’s terminal.

The server is a daemon

The server is started with socketpair() + fork() + daemon() (proc.c):

c
pid_t
proc_fork_and_daemon(int *fd)
{
	pid_t	pid;
	int	pair[2];

	if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, pair) != 0)
		fatal("socketpair failed");
	switch (pid = fork()) {
	case -1:
		fatal("fork failed");
	case 0:
		close(pair[0]);
		*fd = pair[1];
		if (daemon(1, 0) != 0)
			fatal("daemon failed");
		return (0);
	default:
		close(pair[1]);
		*fd = pair[0];
		return (pid);
	}
}

daemon(1, 0) means: detach from the controlling terminal, but keep the current working directory. The server ends up in its own session with no controlling terminal.

That’s the point. A server with a controlling terminal dies when the terminal goes away. A server without one keeps running, and so do all the programs running in its panes.

The race between two clients both trying to start a server is handled with a lock file. client_get_lock() in client.c takes an flock() on <socket>.lock, and only the winner starts a server:

c
		locked = 1;
		goto retry;

with a comment explaining the subtlety:

c
			/*
			 * Always retry at least once, even if we got the lock,
			 * because another client could have taken the lock,
			 * started the server and released the lock between our
			 * connect() and flock().
			 */

The protocol is imsg, and it carries file descriptors

tmux uses the OpenBSD imsg library (bundled in compat/imsg.c) for the client/server protocol. The message types are in tmux-protocol.h:

c
/* Protocol version. */
#define PROTOCOL_VERSION 8

/* Message types. */
enum msgtype {
	MSG_VERSION = 12,

	MSG_IDENTIFY_FLAGS = 100,
	MSG_IDENTIFY_TERM,
	MSG_IDENTIFY_TTYNAME,
	MSG_IDENTIFY_OLDCWD, /* unused */
	MSG_IDENTIFY_STDIN,
	MSG_IDENTIFY_ENVIRON,
	MSG_IDENTIFY_DONE,
	MSG_IDENTIFY_CLIENTPID,
	MSG_IDENTIFY_CWD,
	MSG_IDENTIFY_FEATURES,
	MSG_IDENTIFY_STDOUT,
	MSG_IDENTIFY_LONGFLAGS,
	MSG_IDENTIFY_TERMINFO,

	MSG_COMMAND = 200,
	MSG_DETACH,
	...

The message numbering is meaningful: 100-series is the handshake, 200-series is session lifecycle and commands, 300-series is file transfer (run-shell output, #{...} job output, and the pipe-pane machinery).

The version is checked on every message:

c
	version = imsg->hdr.peerid & 0xff;
	if (imsg->hdr.type != MSG_VERSION && version != PROTOCOL_VERSION) {
		proc_send(peer, MSG_VERSION, -1, NULL, 0);
		peer->flags |= PEER_BAD;

The interesting part of imsg for tmux is that it can pass file descriptors along with messages. proc_add_peer() enables it:

c
	imsgbuf_init(&peer->ibuf, fd);
	imsgbuf_allow_fdpass(&peer->ibuf);

The client hands over its terminal

This is the mechanism that makes the whole design work. The client’s handshake includes its stdin and stdout:

c
	if ((fd = dup(STDIN_FILENO)) == -1)
		fatal("dup failed");
	proc_send(client_peer, MSG_IDENTIFY_STDIN, fd, NULL, 0);
	if ((fd = dup(STDOUT_FILENO)) == -1)
		fatal("dup failed");
	proc_send(client_peer, MSG_IDENTIFY_STDOUT, fd, NULL, 0);

and the server picks them up:

c
	case MSG_IDENTIFY_STDIN:
		if (datalen != 0)
			return (-1);
		c->fd = imsg_get_fd(imsg);
		log_debug("client %p IDENTIFY_STDIN %d", c, c->fd);
		break;
	case MSG_IDENTIFY_STDOUT:
		if (datalen != 0)
			return (-1);
		c->out_fd = imsg_get_fd(imsg);
		log_debug("client %p IDENTIFY_STDOUT %d", c, c->out_fd);
		break;

The server now holds the client’s terminal file descriptor. When the server renders the screen, it writes the escape sequences straight into the user’s terminal. The client isn’t in the data path for output at all.

This has three consequences:

  • Latency. Every byte of screen output crosses the socket? No — it doesn’t. The server writes directly to the terminal fd it was given. The socket only carries commands into the server.
  • The client is replaceable. Detach and re-attach is a new client handing over a new terminal fd. The server keeps the session.
  • Multiple clients. Two people attach to the same session; the server renders for each client separately, at each client’s size, with each client’s terminal capabilities.

The server also learns what the terminal can do

The client sends its terminfo capabilities to the server:

c
	for (i = 0; i < ncaps; i++) {
		proc_send(client_peer, MSG_IDENTIFY_TERMINFO, -1,
		    caps[i], strlen(caps[i]) + 1);
	}

The client reads terminfo using ncurses (setupterm() and tigetstr() in tty-term.c) and ships the ~100 capabilities tmux cares about. The server then builds its own struct tty_term from that list.

This inversion is important: the client does no rendering and no escape sequence translation. It’s a transport. The server is the thing that knows about terminals.

Two kinds of client

The server distinguishes two client personalities, and they behave completely differently.

Attach clients have a terminal. The server puts the terminal into raw mode, renders the composited screen into it, and forwards keystrokes into the active pane:

c
	if (c->flags & CLIENT_CONTROL)
		control_start(c);
	else if (c->fd != -1) {
		if (tty_init(&c->tty, c) != 0) {
			close(c->fd);
			c->fd = -1;
		} else {
			tty_resize(&c->tty);
			c->flags |= CLIENT_TERMINAL;
		}

Note that a control client never gets a tty at all — it’s excluded from the screen-drawing path entirely:

c
	if (c->flags & (CLIENT_CONTROL|CLIENT_SUSPENDED))
		return;

Control clients (tmux -C) speak a text protocol. They never render anything. They send commands, ask for pane output with %output, and receive notifications such as %window-renamed. This is the interface used by editors and status bars, and it’s covered in tmux Control Mode Is an Unauthenticated Command Channel.

Why the split is worth it

The alternative design — a single process that owns everything — is simpler in the abstract, and it’s roughly what screen(1) does. The daemon design buys five things:

1. Sessions outlive the terminal. tmux detach, close the laptop, reopen it tomorrow: the programs are still running, the scrollback is still there. With a single-process design, the multiplexer dies with the terminal and takes the programs with it.

2. Sharing. Several users can attach to the same session. The server renders separately for each client, so one person on a 4K monitor and another on a phone both get a correct view.

3. The server can outlive the client’s connection. A client on a flaky SSH link can be disconnected and reconnected without disturbing the session. tmux even has attach -d and the detach-on-destroy option for exactly this.

4. Control mode falls out for free. Because the server is a server, it can have a client that is not a terminal at all. A scripting interface isn’t a special case bolted on; it’s just another client.

5. The server is the right place for the event loop. A single event loop in the daemon serves every client and every pane. A client that disconnects takes nothing with it.

The costs

The split isn’t free, and the code shows where it hurts:

  • The server must trust the client’s claims about its terminal. The client says “here are my terminfo capabilities” and the server believes them. A lying client gets a wrong screen.
  • The server holds file descriptors for clients. When a client dies without cleaning up, the server has a stale fd. The CLIENT_EXIT and “do not attempt to write to control clients which have been destroyed” fixes are the accumulated scar tissue from this.
  • Every command crosses a process boundary. tmux new-window from a script is a round trip through a socket, not a function call. For a script that runs thousands of commands, that matters — which is why tmux -C exists as a way to keep a persistent connection.
  • The client and server can disagree. The client thinks the terminal is 80x24; the server thinks it is 100x30. The resize protocol exists to reconcile this:
c
	case MSG_RESIZE:
  • Debugging is harder. tmux has no debugger-friendly separation between “the terminal” and “the multiplexer”.

Wrapping up

The classic mistake in writing a terminal multiplexer is to make the multiplexer be the terminal: one process that owns a terminal, parses what programs write, and draws a picture. That works, and it’s what screen does.

tmux’s insight is that the multiplexer should be a service that owns resources (ptys, sessions, scrollback) and clients that provide endpoints (terminals, or script interfaces).

Once the multiplexer is a service, everything else — sharing, persistence, scripting, remote attachment — is a consequence rather than a feature.

TmuxArchitectureInternalsIpc