Hermes TUI benchmark — Ink (current UI) vs OpenTUI (new engine)

Both UIs were run as real binaries in a real terminal, fed the exact same scripted conversations by a fake gateway, and measured from outside the process. Every number below is the typical of repeated runs (median) unless said otherwise. 103 result files · sha 50e34713b · node v26.3.0 · generated 2026-06-11T04:02:33.335Z

The verdict — who won what

dimensionwinnerthe numbers
Memory — typical real sessions (20–300 msgs)Ink wins, modestlyOpenTUI uses ~59–88MB more (163 vs 222MB at 100 msgs; 180 vs 268MB at 300).
Memory — longest real sessions (~2,000 msgs, the longest 1 in 100 = p99)Ink wins big234MB vs 671MB peak — 2.9× more. Sessions this long really happen (6 of them in the DB).
Memory — 3,000-msg stress marathon (beyond any real session)Ink winsInk levels off near 249MB; OpenTUI climbs to ~767MB (peak ~875MB), and its syntax styling degrades past ~1,400 rows. Stress test only — past the longest real session.
Scroll responsiveness on a long transcriptOpenTUI wins decisivelySlowest 1-in-100 scroll responses (p99): 14–17ms vs 83–101ms across repeats. Typical scrolls are ~2ms on both — the difference is the stutters.
Frame smoothness while streamingOpenTUI wins22 vs 16 screen updates/s, and its worst pauses between updates are half as long (103ms vs 209ms, slowest 1 in 20).
Typing echo (keystroke → it appears)TieBoth answer a keystroke in 1–2ms — under any human threshold.
Submit → first reply paintInk wins44ms vs 107ms from pressing Enter to the first reply text on screen.
CPU, including the terminal-emulator (tmux) sideTie~80 CPU-seconds either way for the same 800-message stream (83 vs 80s total); the tmux leg is ~0.4s for both.
Crash recovery (gateway shot mid-stream)TieBoth auto-respawn the killed gateway and end with the full transcript intact and zero orphan processes. Ink respawns in ~81ms, OpenTUI in ~1.0s.
StartupInk wins, modestlyFirst paint 67ms vs 127ms. Both feel instant; OpenTUI actually reaches “session ready” slightly sooner (176 vs 202ms).

  red = Ink (current UI) wins  ·    green = OpenTUI (new engine) wins  ·    grey = tie

One-line summary: OpenTUI is the smoother UI (scrolling, streaming) and Ink is the lighter one (memory, first paint). Everything else is a wash — including reliability, where both recover from a killed gateway with the transcript intact.

Memory at real workloads — what sessions actually look like

The memory debate was framed around 200–300-message sessions. The real session database says that band is the top 5–10%: the typical session is ~20 messages, 90% stay under 182, and the longest real sessions reach ~1941 messages (the longest 1 in 100 — p99) — and at that tail the memory gap widens to 2.9×.

How long the user's real terminal sessions actually are (444 sessions) 0 37 74 111 148 185 number of sessions 168 0–10 78 10–25 78 25–50 47 50–100 30 100–200 14 200–300 18 300–500 5 500–1000 6 1000–3000 messages in the session half are ≤20 (p50) 75% ≤53 90% ≤182 95% ≤340 99% ≤1941 (p99) Every TUI/CLI session in the real session DB (/home/daimon/.hermes/state.db); message counts per session.
Takeaway: real sessions are short — half end within ~20 messages; the 200–300-msg sizes the debate assumed are actually the top 5–10% of sessions.
Peak memory at real session sizes — Ink vs OpenTUI 0.0 158 317 475 633 791 peak memory (MB) 163 Ink 222 OpenTUI 100 msgs (heavy-ish day) 180 Ink 268 OpenTUI 300 msgs (top 5% of sessions) 234 Ink 671 OpenTUI 2,000 msgs (longest real sessions) Peak resident memory of the UI process (VmHWM), typical of 2 repeats (median).
Takeaway: at everyday sizes the gap is a modest 60–90MB; at the rare-but-real 2,000-msg session it becomes 234MB vs 671MB — 2.9× — which is where Ink genuinely wins.
session sizewhat that means in practiceInk peakOpenTUI peakdifference
100 msgsa heavier-than-usual day (typical session is ~20 msgs)163 MB222 MBOpenTUI +59 MB (1.4×)
300 msgstop ~5% of real sessions180 MB268 MBOpenTUI +88 MB (1.5×)
2,000 msgsthe longest sessions that actually occur (~1 in 100, p99)234 MB671 MBOpenTUI +437 MB (2.9×)

Scroll responsiveness — where OpenTUI wins

What fraction of scroll responses finished within X ms (2000-msg transcript) 0 19 37 56 74 93 111 0 20 40 60 80 100 time from scroll input to first screen response (ms) % of scroll responses at least this fast Ink (1350 scrolls) OpenTUI (1350 scrolls) OpenTUI (no cap) (1350 scrolls) Mouse wheel fired 30×/s for 15s, three repeats pooled. Higher curve further left = more responses answered fast.
Takeaway: both feel identical on a typical scroll (~2ms), but Ink’s slowest 1-in-100 responses (p99) take 82–101ms — visible hitches — while OpenTUI stays under ~17ms.

Frame smoothness while streaming — where OpenTUI wins

Frame smoothness while text streams in 0.0 5.3 10.5 15.8 21.1 26.3 frames per second 15.8 Ink 22.3 OpenTUI screen updates per second (higher = smoother) 800-message stream at 30 events/s; a frame = a burst of terminal output separated by a ≥4ms gap.
Takeaway: while a long reply streams in, OpenTUI repaints ~22×/s vs Ink’s ~16×/s — text appears noticeably more fluid.
Pauses between screen updates while streaming (lower = steadier) 0.0 49.3 98.6 148 197 247 gap between frames (ms) 42.0 Ink 39.0 OpenTUI typical gap (p50) 209 Ink 103 OpenTUI slowest 1 in 20 gaps (p95) Same 800-message stream. The right-hand pair is the stutter you actually notice.
Takeaway: typical pauses are similar, but Ink’s worst stutters between repaints (slowest 1 in 20, p95) are twice as long — 209ms vs 103ms.

Typing echo & first reply paint

UIkeystroke echo, typical (p50)keystroke echo, slowest 1 in 20 (p95)Enter → first reply text on screenkeystrokes verified
Ink 1 ms1.5 ms 44 ms30/30
OpenTUI 2 ms3.5 ms 107 ms30/30

Keystroke echo is a tie — 1–2ms on both, far below anything a human can perceive. The one real difference: after pressing Enter, Ink paints the first reply text in 44ms vs OpenTUI's 107ms. (Single run per UI.)

CPU cost of streaming — including the terminal's side of the work

Total CPU burned streaming the same 800-message conversation 0.0 19.5 38.9 58.4 77.8 97.3 CPU seconds 82.4 UI 0.5 gateway 0.4 tmux Ink 79.1 UI 0.3 gateway 0.4 tmux OpenTUI Whole pipeline measured inside tmux: UI process + gateway + the tmux server that has to parse the UI’s output.
Takeaway: a tie — same 800-message stream costs ~80 CPU-seconds on either UI, and the terminal emulator’s share (tmux) is a rounding error (~0.4s) for both.

The hypothesis that Ink's bigger output stream costs meaningfully more CPU in the terminal emulator did not hold at this workload: Ink did push more bytes (3.8MB vs 3.3MB), but the tmux server burned ~0.4 CPU-seconds either way.

Streaming at 30 events/s: terminal output volume and CPU cost per event 0.0 13.9 27.9 41.8 55.7 69.7 output KiB/s · CPU ms per event 58.7 KiB/s 30.1 ms/event OpenTUI (no cap) 58.9 KiB/s 30.0 ms/event OpenTUI 44.0 KiB/s 31.0 ms/event Ink Typical of 3 runs (median); whisker = middle half. CPU is the UI process only, measured over the stream.
Takeaway: per streamed event the CPU cost is in the same ballpark for all configs — neither UI is the cheap one on CPU.

Crash recovery — we shot the gateway mid-stream and watched what happened

Each scenario kills, freezes, or yanks something out from under the UI on a live session, then checks: did the UI survive, did the gateway come back, did the stream resume, is the final transcript identical to an undisturbed run, and is anything left running afterwards?

what we didInkOpenTUI
gw-kill-stream
shot the gateway (kill -9) while reply text was streaming
UI survived: yes
gateway respawned: yes (87ms)
stream resumed: yes
transcript intact: yes
orphan processes: none
UI survived: yes
gateway respawned: yes (1.0s)
stream resumed: yes
transcript intact: yes
orphan processes: none
gw-kill-tool
shot the gateway (kill -9) in the middle of a tool call
UI survived: yes
gateway respawned: yes (81ms)
stream resumed: yes
transcript intact: yes
orphan processes: none
UI survived: yes
gateway respawned: yes (1.0s)
stream resumed: yes
transcript intact: yes
orphan processes: none
gw-stop
froze the gateway for 30 seconds mid-session, then let the UI recover
UI survived: yes
gateway respawned: yes (35ms)
stream resumed: yes
transcript intact: yes
orphan processes: none
UI survived: yes
gateway respawned: yes (1.1s)
stream resumed: yes
transcript intact: yes
orphan processes: none
resize-storm
resized the window 30 times in 3 seconds
survived: yes
transcript intact: yes
orphan processes: none
survived: yes
transcript intact: yes
orphan processes: none
pty-eof
closed the terminal out from under the UI (should exit cleanly, leave nothing behind)
exited cleanly: yes
gateway cleaned up: yes (100ms)
orphan processes: none
exited cleanly: yes
gateway cleaned up: yes (101ms)
orphan processes: none

Result: a tie. Both UIs auto-respawn a killed gateway and finish with a byte-identical final transcript and zero orphan processes. Ink respawns faster (~35–87ms vs ~1.0s), but both are well within "didn't lose anything".

Startup

Startup: time until something is on screen, and until the session is ready 0.0 47.7 95.3 143 191 238 ms after launch (lower = faster) 66.5 first paint 202 session ready Ink 127 first paint 176 session ready OpenTUI Typical of 10 launches (median); whisker = middle half of runs. "first paint" = first byte drawn to the terminal.
Takeaway: Ink gets pixels on screen first (~67ms vs ~127ms); OpenTUI finishes its session bootstrap slightly sooner (~176ms vs ~202ms). Both feel instant.

Stress appendix — beyond real usage

Everything below streams 3,000–10,000 messages into one session — past the longest session ever recorded in the real database (~1941 msgs). It shows where the engines break, not what daily use feels like.

Memory used as the conversation grows (stress runs, every repeat shown) 0 1667 3333 5000 6667 8333 10000 0 464 929 1393 1858 2322 messages streamed into the session memory (MB) hard memory cap 2048 MB OpenTUI OpenTUI (no cap) Ink × crash @3000 × crash @2901 2GB hard cap (systemd cgroup). × = killed for running out of memory. Crash marks on older OpenTUI runs are the exit-7 bug, fixed in the latest runs.
Takeaway: Ink stays flat (~250MB) no matter how long the marathon runs; OpenTUI climbs toward ~870MB and only levels off thanks to its rolling row cap. Neither hits the 2GB kill line.

Stress-run numbers (3,000-msg marathons + one 10,000-msg run)

configmemory growth
MB per 1k msgs
(3,000-msg runs)
memory growth
MB per 1k
(10,000-msg run)
settled memory MB
(plateau, end of run)
peak memory MBscroll ms — typical /
slowest 1 in 10 /
slowest 1 in 100
(p50/p90/p99)
CPU ms per event
(paced stream)
killed by 2GB cap?
Ink 25.48 [middle half: 23.89–27.74] 5.88 249 [middle half: 247–250] 257 [middle half: 256–258] 2.0 / 36.1 / 98.5 31.04 [middle half: 30.99–31.23] none
OpenTUI 254.74 [middle half: 250.81–265.46] not run 767 [middle half: 741–799] 875 [middle half: 868–879] 2.0 / 3.0 / 17.0 30.00 [middle half: 30.00–30.05] none
OpenTUI (no cap) 297.65 [middle half: 283.88–311.91] 173.76 807 [middle half: 796–813] 890 [middle half: 874–893] 2.0 / 3.0 / 14.0 30.09 [middle half: 30.09–30.10] none

Reading guide: "typical" = median; the bracketed range is the middle half of runs (interquartile range). "slowest 1 in 100" = p99. OpenTUI's syntax styling visibly degrades past ~1,400 rendered rows in these marathons (style-handle exhaustion fallback); older OpenTUI runs also show a post-stream crash that was fixed before the latest runs.

Live UI nodes during the 3,000-msg marathon (diagnostic run) 0 500 1000 1500 2000 2500 3000 0 84 169 253 337 421 messages streamed into the session live nodes Ink live layout nodes Diagnostic instrumented runs only — never used for the headline numbers.
Takeaway: Ink keeps a bounded few hundred nodes mounted no matter how long the transcript gets — consistent with its flat memory line above. (No OpenTUI node-walk run in this result set.)

Low-memory survival (1GB Docker container)

cellUImemory limitresultmsgs survivedpeak memorybasis
e3lite-1gInk1 GB completed10000 223 MB
e3lite-1gOpenTUI1 GB died3000 849 MB

Run health — can you trust these numbers?

Determinism gate: each UI replayed the same input twice and must produce a pixel-identical final screen (same fingerprint). If this fails, none of the comparisons above are meaningful.

UIreplay fingerprints (same input must give same screen)gate
Ink7775bee02e57da2b · 7775bee02e57da2b · 7775bee02e57da2b · 7775bee02e57da2b · 7775bee02e57da2b · 7775bee02e57da2b · 7775bee02e57da2b · 7775bee02e57da2b · 7775bee02e57da2b · 7775bee02e57da2bPASS
OpenTUId5e9558583159eac · d5e9558583159eac · d5e9558583159eac · d5e9558583159eac · d5e9558583159eac · d5e9558583159eac · d5e9558583159eac · d5e9558583159eac · d5e9558583159eac · d5e9558583159eacPASS

⚠ some runs were flagged: the test rig itself lagged (results kept, but read with care):

runmax rig laglags >10ms
2026-06-10T2058-50e3471-mem3000-opentui-otui-capped-r0.json18 ms2
2026-06-10T2058-50e3471-mem3000-opentui-otui-uncapped-r0.json22 ms3
2026-06-10T2059-14ee1a5-mem3000-opentui-otui-uncapped-r1.json33 ms3
2026-06-10T2100-197d499-mem3000-opentui-otui-capped-r2.json22 ms3
2026-06-10T2100-197d499-mem3000-opentui-otui-uncapped-r2.json19 ms2
2026-06-10T2107-197d499-cpu800-opentui-otui-uncapped-r0.json11 ms1
2026-06-10T2108-197d499-cpu800-opentui-otui-capped-r0.json15 ms1
2026-06-10T2116-197d499-cpu800-opentui-otui-capped-r1.json11 ms1
2026-06-10T2124-197d499-scroll2000-opentui-otui-capped-r0.json15 ms2
2026-06-10T2125-197d499-scroll2000-opentui-otui-uncapped-r0.json26 ms2
2026-06-10T2126-197d499-scroll2000-opentui-otui-capped-r1.json23 ms2
2026-06-10T2126-197d499-scroll2000-opentui-otui-uncapped-r1.json21 ms2
2026-06-10T2127-197d499-scroll2000-opentui-otui-uncapped-r2.json15 ms2
2026-06-10T2128-197d499-scroll2000-opentui-otui-capped-r2.json20 ms2
2026-06-10T2146-197d499-slope10000-opentui-otui-uncapped-r0.json25 ms4
2026-06-10T2147-e3-e3lite-1g-opentui-otui-capped-r0.json14 ms3
2026-06-10T2236-a939c9a-mem3000-opentui-otui-capped-r0.json24 ms4
2026-06-10T2237-a939c9a-mem3000-opentui-otui-uncapped-r0.json14 ms2
2026-06-10T2237-a939c9a-slope10000-opentui-otui-uncapped-r0.json69 ms17
2026-06-11T0240-805e080-mem2000-opentui-otui-capped-r0.json25 ms2
2026-06-11T0325-cbe703c-chaos-ink-ink-rgw-stop.json13 ms1

Methodology: docs/plans/opentui-bench-suite.md. Real binaries on a real PTY (120×40), fake gateway via HERMES_PYTHON (zero UI changes), outside-the-process /proc sampling, 2GB cgroup caps via systemd. Instrumented diagnostic runs are flagged and never headlined.