All stories
Engineering

Pi-Bolt: the same Pi, compiled to native code

How we compile the Pi coding agent ahead of time into one native executable, and why most of the speed came from doing less work: 112× faster large file writes and 13× less CPU while streaming than Pi on Node.

Lines of source code become bytecode cells and then lime machine-code bars, before launch

A coding agent is the program you leave running all day. It sits in a terminal pane, streams answers you only half read, writes files you didn't ask to see, and keeps a conversation open for hours. So the cost that matters isn't one benchmark. It's how much CPU, memory and patience the agent spends while it waits on the model.

Pi is the coding agent we use every day. It's small, it's extensible, and it gets out of the way. It also runs on Node or Bun, so every launch parses its JavaScript again. And as we found out, a long answer, a large file or a long session makes it redo far more work than it needs to.

Pi-Bolt is our answer to that. It's the same Pi, compiled ahead of time to native code: same commands, same extensions, same ~/.pi/agent configuration. It's open source and MIT licensed, and runs on Linux (x86-64 and ARM64), macOS on Apple silicon and Windows. Android, in Termux, is supported but not benchmarked.

Three ways to run Pi: npm package on Node.js or Bun, PiG ported to Go, and Pi-Bolt compiling Pi's own code into one native executable

Why not a rewrite

There are good ways to make Pi native. PiG ports Pi to Go, with a translation system that follows each Pi release. In our runs it starts sooner and uses less memory than Pi-Bolt: on Linux x86-64 it's ready to type in 15 ms against our 42, with a 66 MB peak against 154 (Pi-Bolt 0.9.6, PiG 0.5.1). We have a lot of respect for how carefully it's built.

We wanted something different: Pi's own code, unchanged in behavior, so that every Pi extension, theme and setting keeps working, and each Pi release can become a Pi-Bolt release without a port to catch up. That ruled out a rewrite. It left one question: can you compile a TypeScript program the way you compile a C program, once, before anyone runs it?

How it works

Pi-Bolt's build takes a Pi release, with a small set of fixes on top, and turns it into one executable.

Pi-Bolt build: Pi release plus fixes, Bun to bytecode, AOT compiler via B3, prebuilt heap and training profile, one executable mapped at launch

  1. Bun bundles Pi and compiles it to JavaScriptCore bytecode.
  2. An ahead-of-time compiler turns every function into optimized machine code through B3, the back end of JavaScriptCore's top-tier JIT. It builds on oven-sh/WebKit#743. We ported it to x86-64 and macOS and added our own code generation and runtime work.
  3. A prebuilt heap holds Pi's modules already loaded, so they don't have to be evaluated at launch.
  4. A training profile orders the code so that startup touches as few pages of the file as possible.

At launch there is nothing to parse and nothing to warm up. The executable maps its code and heap from the file and runs main().

Time from Enter to a prompt on Linux x86-64: Pi on Node 24 411 ms, Pi on Bun 382 ms, Pi-Bolt 45 ms

That's the part everyone expects from "compiled to native code". It's also the smallest part of the story.

The speed was mostly in doing less work

When we measured where Pi's CPU actually went, launch was the small part. The real cost was work that grew faster than the task itself. Three places stood out.

Streaming an answer

A model streams its answer a few words at a time. Each time, stock Pi redraws the message: it re-lexes the whole Markdown, re-wraps every block and re-highlights every code block. The first chunk is cheap. The thousandth chunk redoes everything the first 999 did.

Per streamed chunk, stock Pi redoes every block; Pi-Bolt redoes the last block or two. CPU 37.9 s vs 2.9 s; tmux output 3,563 KB vs 528 KB

Pi-Bolt keeps what it has already rendered and redoes only the last block or two, with highlighted code cached. The screen shows the same output, line for line. For a 60,000-character answer, that's 2.9 seconds of CPU instead of 37.9 on Node 24. In tmux, the same change cuts what Pi sends to the terminal for a 20,000-character answer from 3,563 KB on Node 24 to 528 KB.

Writing a large file

A model writes a file by streaming the write tool call's arguments, the whole file, a few characters at a time. A 400 KB file arrives in thousands of chunks. For each one, stock Pi parses the entire tool call again, redraws the screen even when nothing shown has changed, and highlights every line of the file.

Pi-Bolt parses the arguments again only once they've grown by an eighth, and once in full when the call completes. It draws a frame only when what's shown changes, and highlights only the lines a collapsed call shows. On Linux x86-64, writing that 400 KB file takes 1.3 seconds, against 146 seconds on Node 24 and 182 on Bun.

Long sessions

Every request sends the whole conversation to the model again. Stock Pi serializes all of it from scratch each time, so each prompt costs more than the last.

Stock Pi serializes every message per request; Pi-Bolt reuses the previous request's bytes. Same bytes sent; CPU per prompt 362 ms vs 65 ms

Pi-Bolt builds each request from the previous one's bytes plus what's new. The bytes sent to the model are identical; the work isn't. Between prompts 21 and 40 of a session, Pi-Bolt spends 65 ms of CPU per prompt, against 362 ms on Node 24.

None of these fixes needs a compiler, and that matters for how we measure. Pi-Bolt's code on Bun would carry them too, so "Pi on Bun" in our benchmarks is Pi as released: the Pi you'd actually install.

The numbers

Every build does the same work against the same local model, which replays the same responses, so there's no network or provider noise. Builds take turns on the same machine. We ran Pi-Bolt 0.9.7 against Pi 1.3.0 as released on npm, run by stock Bun 1.4.2, Node 22 and Node 24, on GitHub's Linux x86-64, Linux ARM64, macOS and Windows runners.

Times less Pi-Bolt needs than Pi on Node 24 for six measurements on Linux x86-64, Linux ARM64, macOS and Windows, from 2.3× to 112×

On Linux x86-64, pinned to one CPU, against Pi on Node 24. The long answer is 60,000 characters, CPU per prompt is the average over prompts 21 to 40, and memory is after 40 prompts:

On Linux x86-64Pi-BoltNode 24
400 KB file write1.3 s146 s
CPU, long answer2.9 s37.9 s
CPU per prompt65 ms362 ms
CPU per session190 ms1,179 ms
Memory166 MB578 MB
Ready to type45 ms411 ms

PiG still starts sooner and uses less memory: a Go binary doesn't carry a JavaScript engine. Pi-Bolt's advantage is that it runs Pi's own code, so everything built for Pi works unchanged. The raw data, the scripts and the method are in the repository's BENCHMARKS.md.

Extensions and the JIT build

Pi's own code is compiled to machine code. An extension you install at run time isn't: the default build has no JIT, so it's interpreted. Light extensions don't notice. Heavy ones do, so there are two ways out.

Word-counting extension in Pi-Bolt 0.7.3 on Linux: default build interpreted 1,315 ms, JIT build 48 ms, pi-bolt compile 55 ms

  • pi-bolt compile compiles the extensions installed on your machine into your Pi-Bolt, with the release's own compiler. pi-bolt update compiles them into each new release.
  • The JIT build turns the JIT back on for extensions loaded at run time, including ones that call native libraries through bun:ffi. Pi itself stays compiled. In our plugin benchmark, a word-counting loop takes 1,315 ms interpreted, 48 ms with the JIT build and 55 ms compiled in (Pi-Bolt 0.7.3, Linux).

The installer also offers three extensions we maintain for Pi-Bolt: subagents, a live todo list and fast file search. It asks before installing any of them.

Release signing

Every release's checksums are signed with an Ed25519 release key. The installer and pi-bolt update check the signature and the checksum before installing anything, and pi-bolt update --rollback puts back the build an update replaced. pi-bolt doctor prints a plain-text report of your installation to paste into an issue, without keys or tokens.

Try it

On Linux, macOS and Android (Termux):

curl -fsSL https://pi-bolt.opensec.in/install.sh | sh

On Windows, in PowerShell:

irm https://pi-bolt.opensec.in/install.ps1 | iex

It installs next to Pi as pi-bolt and uses your configuration as it is, so you can try it on today's work and switch back anytime.

Pi-Bolt is on GitHub, and the documentation is at pi-bolt.opensec.in. If something feels slow in your setup, open an issue and tell us what you were doing. We'll measure it, and that's usually how the next fix starts.

OpenSec

The team building OpenSec.

A clearer view of security.

Research, engineering notes, and updates from OpenSec.

Subscribe to the dispatch