I asked macOS for a megabyte. It gave me a kilobyte. Then it did that 4,095 more times.
That was a twelve-line Python script on my Mac last night, reading from a pseudo-terminal: the same device every terminal app reads your shell's output from. No terminal app involved, just the kernel.
I ran it because of Ghostty 1.4, released Oct 27 (753 points and 226 comments on HN at 6:30 PM PT that day). Its release notes credit this ceiling for the biggest speed win. I didn't install Ghostty, so every Ghostty number below is the maintainers' benchmark. The kernel part I checked myself.
The 1 KiB ceiling, reproduced
A child process writes 4 MiB to a pty in one call. The parent reads with a 1 MiB buffer and counts what each read returns:
import os, pty, time, collections
N = 4 * 1024 * 1024 # 4 MiB
pid, fd = pty.fork()
if pid == 0: # child: one big write to the pty
os.write(1, b'x' * N); os._exit(0)
sizes = collections.Counter(); t0 = time.time()
while True:
try: b = os.read(fd, 1024 * 1024) # parent: ask for 1 MiB at a time
except OSError: break
if not b: break
sizes[len(b)] += 1
print(sum(sizes.values()), 'reads; sizes', dict(sizes), '; %.2fs' % (time.time() - t0))
What I got on macOS 26.7.1, Apple Silicon, Python 3.15, three of five runs in a row (the other two matched):
4096 reads; sizes {1024: 4096} ; 0.04s
4096 reads; sizes {1024: 4096} ; 0.03s
4096 reads; sizes {1024: 4096} ; 0.04s
Every read, exactly 1,024 bytes. The buffer size doesn't matter. Then I swapped the pty for a plain pipe (os.pipe() plus os.fork(), same loop): 64 reads, every one 65,536 bytes. Same data, same machine, 64 times fewer trips into the kernel.
The read counts never moved, run after run. The timings did: 0.03 s on a quiet machine, up to 0.47 s while other work was running. So trust the counts, not my clock.
Why a small read costs more than it looks
A kilobyte per read is not slow by itself. My script did 4,096 of them in a few hundredths of a second. The cost shows up when you do real work after each one.
The pull request behind the fix spells out what Ghostty used to do: one loop that read from the pty, then processed what it read, then read again. Each pass paid fixed costs (taking the terminal lock, waking the renderer) no matter how few bytes came in. And while the loop was busy parsing, nobody was draining the pty, so the program producing the output, cat say, had to wait for room in the kernel's queue.
Two problems, one cause: a single thread doing two jobs.
I wanted to feel that, so I wrote a toy. The child writes 4 MiB and reports how long its write was stuck. The parent fakes a "parse" with a 100 microsecond sleep. In one mode it parses after every read, on the same thread. In the other, a reader thread only drains the pty into a queue, and a second thread takes whatever has piled up and parses it as one batch. Seven runs each:
one 4096 reads; 4096 parses; writer blocked 0.599s; all parsed 0.60s
two 4096 reads; 253 parses; writer blocked 0.041s; all parsed 0.04s
That pair is typical. Across the seven pairs, one thread kept the writer stuck for 0.59 to 1.15 s. Two threads freed it in 0.04 to 0.11 s, with 233 to 443 batches instead of 4,096 parses.
Don't read 15x into that. My fake parse costs the same per call no matter the size, so batching erases it. Real parsing costs something per byte too, which is why Ghostty measured 25% to 55% more I/O throughput on macOS (M4 Max, against 1.3.x), not 15x. Combined with a much faster parser in the same release, the maintainers report cat of a 150 MB file going from 1.5 s to 566 ms.
The claim: suspect the read loop first
Here is what I take from it. When a terminal chokes on heavy output, the instinct is to blame the terminal: its renderer, its parser, its language. On macOS, look at the read loop first. The PR says the Ghostty team had been happy to find 2% gains in this subsystem in recent years. Then 25% to 55% turned up in how the loop met a 1 KiB pipe.
The cap belongs to the kernel, so it hits anything that reads a pty on macOS: multiplexers, test runners that capture output through a pty, expect-style scripts, editor terminals. I haven't checked how any of those read. But any loop that reads, does real work, then reads again pays its overhead every kilobyte and keeps the writer waiting. The fix is not clever. Split the draining from the processing.
In the PR, Mitchell Hashimoto says he chose the old design in 2023 believing reads are usually small. They are, he writes now, because the kernel cuts them that way.
The other half: memory
The second change is scrollback. Ghostty 1.4 compresses scrollback pages you aren't looking at, while the terminal is idle, and decompresses them when you scroll or search. The notes claim 70% to 90% less resident memory for text-heavy history, so the default scrollback limit went from 10 MB to 50 MB per terminal.
Smaller numbers from the same notes: an empty terminal's page memory on macOS is about 16 KiB, down from 128 KiB. Hidden tabs now give back their GPU resources. With 20 hidden tabs and one visible, GPU memory went from about 385 MiB to 18 MiB.
Two caveats: compression only runs on macOS and 64-bit Linux, and it lowers resident memory, not virtual, so measure the right one.
What to try today
Install from ghostty.org/download. On a Mac, an existing install picks up the update through its built-in updater. Homebrew's cask still listed 1.3.1 when I checked on the evening of Oct 27.
Two commands worth a minute:
ghostty +ssh user@host # installs Ghostty's terminfo on the remote, forwards COLORTERM and friends
ghostty +explain-config font-size # prints the docs for one option, paged
+ssh targets the usual cause of odd behavior over SSH, a complaint repeated in the HN thread: the remote doesn't know xterm-ghostty. If the terminfo install fails, it falls back to xterm-256color. The SSH docs show how to alias plain ssh to it.
The breaking change that will bite Mac users: copy-on-select now defaults to none on macOS, so selecting text copies nothing, and middle-click paste of a selection stops working. To get 1.3's behavior back:
copy-on-select = primary
Use clipboard if you want selections to land on the system clipboard instead. Also on macOS, cmd+up and cmd+down jump between prompts again rather than scrolling to the top and bottom.
Update even if you don't care about speed. The release fixes 10 security advisories. The one I'd flag: with zsh shell integration, a crafted directory name could make the shell send arbitrary escape sequences when you cd into it, for example quietly replacing your clipboard (GHSA-2r63-vmw6-3m3c). Cloning a repo is enough to get such a directory.
Where it falls short
Linux users on older distros lose out. The GTK app now needs GTK 4.18 and libadwaita 1.7, which rules out Ubuntu 24.04 LTS. The notes point those users to the Flatpak or an older Ghostty.
Commenters on HN pushed back, too. One won't leave Kitty until Ghostty has something like Kitty's hints mode. One on NixOS said launch takes seconds, and the launch gains in this release are macOS only. One iTerm2 user on an M1 MacBook Pro tested 1.4 and saw no difference. That one is fair: the pty win shows up under heavy output, and most terminal days aren't that. The thread's recurring question, why build another terminal at all, has an answer in Mitchell Hashimoto's own comment: the reusable core, libghostty, is the point.
Your turn
The 1 KiB number is a macOS fact. The PR says Linux still needs checking. If you run the script above on Linux, what read sizes does your pty give you?
Top comments (0)