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.
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 checks | TypeScript | Rust |
|---|---|---|
| CLI contract (version, help, removed commands, config loading) | 26 / 26 | 26 / 26 |
| Acceptance and real-incident expectations | 6 / 15 | 6 / 15 |
| Total | 32 / 41 | 32 / 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
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
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 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.23 | Rust v0.24 | |
|---|---|---|
| What you install | 26 MB bundle + Node.js runtime (112 MB) | 32 MB single binary |
| Runtime requirement | Node.js 22+ | No Node.js (Linux builds are static) |
| Source-intelligence backend | Python wheel, installed with uv | Same, bundled in every package |
| Cold start (median) | 158 ms | 5 ms |
| Idle memory | 153 MB | 10 MB |
Upgrading
# 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-tsOn 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.