libtmux sends commands to tmux through subprocesses or persistent control-mode connections. Some ports also batch commands into one invocation:
- One-shot subprocess. Each command spawns a fresh
tmuxprocess, which parses argv, executes the command, prints its output, and exits. This is the default in every port, and the only lane in Python: every.cmd()call underneath the object API is asubprocess.Popenaround atmuxinvocation. - A persistent control-mode client.
tmux -C attach-sessionstarts one long-lived tmux process that stays attached and speaks a line-oriented protocol over its stdout: commands go in, replies and asynchronous notifications (%window-add,%output, …) come out, without starting a process per call. - One invocation, several commands. tmux accepts more than one command
per invocation (
;-joined, or one-F-taggedlist-*per line). A port can fold several logical operations into a single process start without opening a control-mode connection at all.
Where each port draws the lineLink to section
| Port | One-shot | Folded invocation | Persistent control client |
|---|---|---|---|
| Python | every call | - | test-only (ControlMode, libtmux._internal) |
| TypeScript | default | pipeline(), batch() | connect() / watch(): notifications only, commands stay per-process |
| Go | process path | Plan.Run | connection (Session.OpenControl), streaming (OpenNotifications) |
| Rust | plan feature, sequential | plan, folded | control-mode feature |
| C# | “One-shot” mode | “Chained” mode (server.Chain()) | “Control” mode (EnterControlModeAsync) |
| C++ | bounded subprocess (default) | Chain | Server::control() → Connection |
| Java | every call | Batch | ControlClient (attach, send, subscribeEvents) |
| Swift | default | - | server.connect() / .watch() (notifications; see below) |
Choose based on whether you need command results, notifications, or a batch of changes.
Notifications and commands are separableLink to section
TypeScript’s connect() adds an event observer while commands such as
session.newWindow(...) and pane.sendKeys(...) continue to run as separate
tmux processes. A dedicated process provides a completion boundary for output
from alias-expanded or waiting commands.
Go’s Session.OpenControl, .NET’s EnterControlModeAsync, Java’s
ControlClient.send, and Rust’s control-mode feature can send commands
through the persistent connection.
Check your port’s transport API before assuming that subscribing to events also
changes how commands run.
A control client is a real clientLink to section
A persistent control connection attaches a tmux client. It appears in
list-clients, increments session_attached, and affects destroy-unattached,
client hooks, and idle-client accounting. Each connection counts separately.
Python’s internal ControlMode test helper uses this behavior for commands that
require an attached client, such as display-popup and detach-client.
Why fold several commands into one invocationLink to section
Creating an object can require a second command to read its resulting state.
Batching reduces those repeated reads and process starts. TypeScript’s batch()
resolves planned mutations from one final snapshot. Go’s Plan, .NET’s Chain,
and C++‘s Chain also group operations without attaching a control client.
What this costs in practiceLink to section
The Rust matrix example and .NET README compare process counts and timings.
Their results describe specific workloads and environments:
- Rust’s
matrixexample runs the same create-and-query workload five ways. Blocking sequential and async sequential both cost 6 processes for 6 dispatches; folding the same 6 into async batches costs 3 processes; routing them over a control-mode connection costs exactly 1. - C#‘s README reports the marginal cost of one more command in each mode, as medians against tmux 3.7b: roughly 2.3 ms for another one-shot process, roughly 0.2 ms for another command over an already-open control client, and roughly 0.02 ms for another command folded into one chained invocation.
A persistent connection avoids starting a client for each command. A chain groups a known sequence into one invocation. For occasional commands, use the default subprocess transport; measure your workload before changing transports for performance. Use a notification stream when your program needs tmux events.
Choosing a laneLink to section
These examples use each port’s subprocess API. See the port reference for batching and control-mode setup.
To inspect tmux’s control protocol, attach a control client:
$ tmux -C attach-session -t work