jk
Blog

Making a terminal on another computer feel local

In my terminal app, opening a terminal on another computer took 1.4 seconds, though the shell there starts in 3 ms. This is where the time went.

3 November 20268 min readTerminalsPerformance

When I started building Kodosi, a terminal app for working with coding agents like Claude Code, there was one thing I wanted to get right before anything else. A terminal running on another computer should feel like it runs on mine. It opens when I click, and what I type shows up while I type it.

Kodosi puts the terminals of all my computers in one window, and I can share a terminal with a friend, who then works in it with me from their own computer. In early October that all worked, but it didn’t feel local. On a slow connection, opening a friend’s terminal took 1.4 seconds, long enough to wonder whether the click did anything.

The shell on that computer starts in 3 milliseconds, so almost none of that time was the shell. Over the next month I went looking for where it went, and later, when a new feature made typing lag, for that too.

Three machines and a round trip

Three machines take part. The host is the computer where the shell runs. The viewer is the device you type on: your laptop, or a friend’s laptop when you’ve shared the terminal. Between them sits Kodosi’s server, which passes the bytes along. It can’t read them, because the host and the viewer encrypt everything between themselves.

Vieweryou type here
Kodosi's serverpasses bytes on, can't read them
Hostruns the shell, 3 ms
Viewerdraws what came back
A keystroke goes from the viewer through the server to the host, and the shell sends it back to be drawn. The shell's part is the shortest step on the path.

A trip there and back is a round trip. Across town it takes a few milliseconds. Between continents, or on a weak phone signal, it can take 200 ms or more. In a terminal, the letter you type appears only when the shell sends it back, which is called the echo, so a keystroke can never show up sooner than one round trip.

Interface research puts the limit for “instant” at about 0.1 seconds (Nielsen Norman Group). I tested on a link slowed down on purpose to a 200 ms round trip, where nothing can feel instant. On a link like that, “local” can only mean that nothing waits besides the round trip itself: a terminal opens in about one round trip and a keystroke echoes in about one. Anything more is waiting I could remove.

Opening a terminal started from scratch every time

Opening a terminal used to go like this, one step after another:

  1. The viewer opened a new connection to the server, with its own encryption handshake.
  2. The server checked that the device was allowed in, as it did for every terminal.
  3. The host opened a new connection to the server too, since every viewer got its own connection on the host’s side as well.
  4. The viewer downloaded its list of trusted devices and their encryption keys from the server, even though it had a copy from the last time. Every device keeps this list to know who it’s talking to.
  5. Only then did the host send the screen.

None of these steps did much work. The cost was that each one waited for the server or the host to answer before the next could start. On the 200 ms link those waits added up to 1.4 seconds.

The fix does each step once. A device now keeps a single connection to the server, and each terminal it opens is a separate encrypted channel inside it, so a new terminal needs no new connection. The viewer keeps the list of keys and asks the server again only after 10 minutes, when a friend is added or removed, or when it meets a device it doesn’t know. When nothing changed, the server’s answer is a few bytes.

The host also sends the screen right away, together with its answer to the request to open. That first screen is only the rows you can see, and the history you can scroll back through follows once the terminal has been quiet for a second. On the 200 ms link a friend’s terminal now opens in 0.23 seconds, one round trip and a little more. At a 100 ms round trip it went from 0.7 seconds to 0.13.

MeasureA new connection per terminalOne connection per device
Time to open, 200 ms round trip1.4 s0.23 s
Time to open, 100 ms round trip0.7 s0.13 s
A terminal on a computer the viewer already knows now opens about six times sooner, in roughly one round trip.

Two costs came with it. The first terminal after the app starts still has to fetch the list of keys, so it takes 0.64 seconds on the 200 ms link. That’s better than 1.4, but well over one round trip. And since all of a device’s terminals now share one connection, which delivers bytes in order, a single lost packet makes all of them wait until it’s sent again.

The shortcut I built and deleted the same day

The other way to cut waiting is to shorten the path. When both computers are on the same network, the viewer could talk to the host directly and skip the server. On 5 October I built exactly that. The host listened on its local network address and sent that address and a random token to the viewer through the encrypted channel. A viewer on the same network then connected straight to the host, with the same encryption checks. It worked between a Mac and a Linux computer in both directions.

The same day I took it out. It helped only two devices on the same home or office network, and they still needed the server to hand over the address. In exchange every host got an open port, macOS asked people for permission to use the local network, and a computer behind a firewall that blocks unknown ports gained nothing. It saved a round trip for a few people and made the app worse for everyone else.

Typing while a picture uploads

With opening fixed, the next slowdown came a month later, from a new feature: pasting pictures. You can paste a picture into a coding agent, a screenshot of a bug for example, and the agent looks at it. But the agent reads the clipboard of the computer it runs on, the host, not the laptop where you pressed paste. So the picture had to reach the host some other way, through whatever clipboard sharing the system had, which is slow and happens out of Kodosi’s sight. It took 40 seconds or more with nothing on screen, so people pasted again. Once, ten pastes arrived six minutes later as ten pictures.

On 3 November I made Kodosi carry the picture itself. The viewer sends it through the terminal’s encrypted channel in small pieces, and the host saves it and pastes the file’s path, which the agent then opens. This time I tested over a real internet link between a Mac and a Linux computer. It was an ordinary home connection, with about 9 Mbit/s of upload and a 70 to 93 ms round trip. At 9 Mbit/s, a large 13.4 MB picture can’t move in much less than 12 seconds, and it took 12.78. An SSH copy of the same file took 12.99.

The speed was right the first time. The typing was not. Since the first fix, all of a device’s terminals share one connection, so a keystroke in any terminal travels behind the picture. On that link, with nothing uploading, a keystroke echoed in 0.07 seconds, about one round trip, as good as that link allows. While the picture uploaded, it took 0.44 to 0.71 seconds.

To keep a link busy, a sender keeps some data in flight: sent, but not yet confirmed by the other side. Data in flight is either moving along the link or waiting in a queue somewhere on the way. The first version kept as much picture in flight as the link carries in half a second, the same limit Kodosi uses for a terminal’s output. That was about six times more than it takes to keep this link busy, and the extra sat in a queue. Every keystroke waited behind it.

So the upload now keeps about two round trips of data in flight, and never more than half a second. About one round trip’s worth is moving along the link, and about one more sits in the queue, so the link never runs dry. A keystroke now queues behind about one round trip of picture.

For the first 3 seconds of the upload that worked, and then the echo crept back up to 0.40 seconds and over. The host confirms every piece it receives, and the viewer timed those confirmations to measure the link’s speed. On this link the confirmations arrived in clumps, several at once, so the link looked faster than it was and the viewer let more picture into flight. Now the host stamps each confirmation with the time it received the bytes. The viewer works out the speed from those times, so it no longer matters how the confirmations bunch up on the way back.

Nothing uploading0.07 s
First version: half a second in flight0.44 to 0.71 s
Two round trips, speed timed by the viewer0.40 to 0.46 s
Two round trips, speed timed by the host0.17 to 0.25 s
How long a keystroke in another terminal takes to echo while a 13.4 MB picture uploads, over the 70 to 93 ms internet link. Each bar runs to the slowest echo of its range.

Typing next to an upload now echoes in 0.17 to 0.25 seconds. That’s still two to three times the 0.07 with nothing uploading, because a keystroke waits behind one round trip of picture and then makes its own round trip.

What I learned

A keystroke can’t beat the round trip, so count everything else that waits. The 1.4-second terminal had no slow code in it. It had a second connection, a handshake and a list the device already had, each waiting for the one before. On a slow link every wait costs a full round trip, and the waits add up long before the code does.

Size what’s in flight in round trips. Half a second sounds small, but on that link it put six round trips of picture in front of every keystroke. Two round trips’ worth keeps a link busy and costs a keystroke about one.

Find where the time goes before you change the path. The direct connection only helps two computers on the same network, and the slow links I had measured were never that. Both fixes here stayed on the path I already had, with no open port.

Where it stands

On an everyday connection, with a 100 ms round trip, a terminal now opens in 0.13 seconds, a little over the 0.1-second line for instant. On the slow 200 ms link it opens in 0.23 seconds, about one round trip. That will never feel local, but it’s as fast as that link allows. A keystroke echoes in one round trip, or in two to three while a picture uploads. The faster opening shipped in Kodosi’s first release, 0.1.1, in October, and picture paste goes out with the next one.

Two things are still open. Terminal output still keeps up to half a second in flight, the amount the picture upload dropped. So on a fast link, a flood of output can still put half a second in front of a Ctrl-C. And if you paste a picture and keep typing, your text reaches the agent before the picture’s path does.

Read next