Blog

Horus is now a single native binary

Horus v0.24 is a command-for-command Rust port of the CLI: the same commands, flags and reports, a 5 ms cold start, and a fraction of the memory. Here is how we measured it.

October 22, 2026The Horus team6 min read

Starting with v0.24, the horus you install from npm, Homebrew or our install script is a native binary written in Rust, and it no longer needs Node.js. The commands, flags, help text and reports are the same as in v0.23. What changes is how much of your machine Horus needs while it works.

We did not want to ask anyone to take that on faith. So before switching, we ran the TypeScript and Rust CLIs side by side against real projects and compared their output, speed and memory, command by command.

How we measured

  • Twelve real repositories. One with a full production setup (logs, metrics, a document database, a job queue and a commerce API), one with Elasticsearch and Sentry, and the rest mostly code-only. Connectors we could not reach live, such as Postgres and Axiom, were compared only on their health checks. On the eleven smaller repositories, queue topology was seeded from the TypeScript output, so those runs compare rendering, not queue discovery.
  • The same input at the same moment. Every case ran the TypeScript CLI, then the Rust CLI, from the same directory against the same live systems. Commands that read live data were also run in a paired mode where both start in the same instant, so they query the same time window.
  • Read-only. Nothing was written to production systems, and Horus Cloud uploads were blocked on both sides for the benchmark.
  • Output first, then speed. Text output was compared line by line after masking timestamps, ids and durations; JSON was compared value by value, including key order; exit codes and stderr were compared too. Timings are the best of three runs (investigations ran once); CPU and peak memory come from /usr/bin/time -l.

All runs were on an Apple silicon MacBook in October 2026, using the release candidate that became v0.24. Linux and Intel Mac builds ship too but were not part of this benchmark. The harness and the full method are in the horus-rust repository.

Same answers

Speed does not matter if the answers change. On the main project, 66 of the 76 cases we could compare produced identical output. Exit codes matched in 79 of 80 cases, counting investigate as a match: there the TypeScript CLI exits 1 only because the benchmark blocks its Cloud upload, which the Rust CLI skips by design. Across the other eleven repositories, output matched 98–100% on average, and exit codes matched once that same upload difference is set aside.

We reviewed every remaining difference. Most come from things that are supposed to differ: each CLI keeps its own local history, live data moves between runs, and two processes never read the clock at exactly the same moment. The one exit-code mismatch was the TypeScript CLI failing on a damaged local store. One difference favours the new build: in the measured investigation, the TypeScript CLI ran past its 30-second metrics budget and dropped those findings, while the Rust CLI parsed the response as a stream and kept them. We found no difference in the reasoning engine itself.

Release checksTypeScriptRust
CLI contract (version, help, removed commands, config loading)26 / 2626 / 26
Acceptance and real-incident expectations6 / 156 / 15
Total32 / 4132 / 41

Both CLIs pass and fail exactly the same release expectations. The failures are cases where Horus itself still falls short (a queue boundary it should resolve, a real incident where it misses the root cause), and the port reproduces them faithfully. Fixing those is product work, not porting work, and it is next.

Starting up

Wall time per command (lower is better)TypeScript v0.23Rust v0.24
--version
TypeScript v0.23: 158 ms
Rust v0.24: 5 ms
doctor
TypeScript v0.23: 350 ms
Rust v0.24: 33 ms
queues
TypeScript v0.23: 360 ms
Rust v0.24: 41 ms
replay (saved report)
TypeScript v0.23: 343 ms
Rust v0.24: 10 ms
explain <symbol>
TypeScript v0.23: 484 ms
Rust v0.24: 109 ms
knowledge search
TypeScript v0.23: 244 ms
Rust v0.24: 49 ms
simulate (offline engine)
TypeScript v0.23: 589 ms
Rust v0.24: 175 ms

Each row is scaled to its own slower value; the right-hand figure is how many times faster Rust is. Best of three runs on the main project; --version is the median of 20 cold starts.

Most of the TypeScript time on these commands was spent before any work started: starting Node, loading the bundle and opening an embedded Postgres store. A native binary skips all of that, so short commands now feel instant. That matters most when an agent or a script calls Horus many times in a row. Across all twelve repositories, the typical speedup per project (geometric mean) ranged from 2.5× to 11.8×.

Doing real work

Commands that talk to production systems (lower is better)TypeScript v0.23Rust v0.24
status (all connectors)
TypeScript v0.23: 25.9 s
Rust v0.24: 13.0 s
queues --live (Redis)
TypeScript v0.23: 12.5 s
Rust v0.24: 5.0 s
logs (24 h, Elasticsearch)
TypeScript v0.23: 1.4 s
Rust v0.24: 1.0 s
investigate (markdown)
TypeScript v0.23: 83.5 s
Rust v0.24: 66.8 s
investigate (json)
TypeScript v0.23: 103.9 s
Rust v0.24: 97.0 s
state (MongoDB scan)
TypeScript v0.23: 38.7 s
Rust v0.24: 38.0 s

Each row is scaled to its own slower value. Best of three runs, except investigate, which ran once; in that run the TypeScript CLI hit its 30 s metrics budget. When a command mostly waits on a remote system, both CLIs wait for the same thing.

Here the gains are smaller, and in places there are none. A large MongoDB scan takes about as long either way, because the database is doing the work, and providers doctor peaks at about 500 MB in both because that time is spent in the Python backend. Full investigations finish 1.1× to 1.3× faster; the Rust CLI also decodes large metric responses as a stream, which keeps it inside the metrics time budget.

Memory

Peak memory per command (lower is better)TypeScript v0.23Rust v0.24
idle (--version)
TypeScript v0.23: 153 MB
Rust v0.24: 10 MB
doctor
TypeScript v0.23: 923 MB
Rust v0.24: 13 MB
explain <symbol>
TypeScript v0.23: 932 MB
Rust v0.24: 20 MB
status
TypeScript v0.23: 951 MB
Rust v0.24: 22 MB
investigate (json)
TypeScript v0.23: 955 MB
Rust v0.24: 143 MB
investigate (markdown)
TypeScript v0.23: 1,279 MB
Rust v0.24: 174 MB

Peak resident memory from /usr/bin/time -l. The right-hand figure is how many times less memory Rust used.

This is the biggest change. Commands that opened the local store peaked near 900 MB in the TypeScript CLI. In Rust, typical commands peak between 15 and 90 MB, and a full investigation stays under 180 MB. Eight explain commands started at once, each opening the local store, needed about 7.5 GB in total in the TypeScript CLI and 163 MB in Rust, which also finished in 0.8 s instead of 2.3 s.

TypeScript v0.23Rust v0.24
What you install26 MB bundle + Node.js runtime (112 MB)32 MB single binary
Runtime requirementNode.js 22+No Node.js (Linux builds are static)
Source-intelligence backendPython wheel, installed with uvSame, bundled in every package
Cold start (median)158 ms5 ms
Idle memory153 MB10 MB

Upgrading

bash
# npm (macOS and Linux, arm64 and x64)
npm i -g @merittdev/horus@latest

# Homebrew
brew upgrade horus

# Installed with the install script? Re-run it
curl -fsSL https://horus.sh/install.sh | bash

# Check you are on the new build
horus --version   # 0.24.x

# Then, once: copy your local history into the new store
horus db migrate-from-ts

On v0.23, horus update checks the old release feed and will report that you are up to date, so use one of the commands above. If horus --version still prints 0.23, an older copy earlier on your PATH is shadowing the new one (which -a horus). Your configuration, connectors and secrets in ~/.horus and .horus/config.json keep working as they are. Local history is the one thing that moves: investigations and memory now live in SQLite (~/.horus/horus-rust.db) instead of the embedded Postgres store (~/.horus/horus.db). Before you run horus db migrate-from-ts, stop any running TypeScript horus commands and the watch service. The migration needs node and npx this one time, to read the old store. It only reads that store, it is safe to run again, and until you run it Horus prints a one-line reminder. Memory you already synced to Horus Cloud comes back on its own.

Release notes and archives for every platform are on the releases page, and the installation guide covers fresh installs.

What is still different

A config written as horus.config.ts or .js is loaded through an installed node; .horus/config.json needs nothing extra. Crash output no longer includes JavaScript stack traces. The full list of intentional differences is in the horus-rust README.

What comes next

The port was the foundation, not the goal. We will keep running this side-by-side benchmark before releases. Next, we want to work on the failures both versions share: resolving queue boundaries more reliably, and seeding investigations from the exact identifiers in an alert (run IDs, error codes, workflow names) before falling back to fuzzy search. If an investigation goes the wrong way, horus feedback and horus report tell us where.

Give your AI agent grounded project memory