Last weekend I read a post by Sarath S titled Rust vs Go vs Zig vs Bun vs Node.js for CLI tools in 2026: I built the same tool five times. At first I assumed it would be another “Rust wins everything” benchmark. The more I read, the stranger it got — the author ended up picking Go.
The important part: he didn’t just theorize. He actually spent a weekend building the same weather CLI, wx, in all five languages. The tool isn’t fancy: geocoding, a weather API, JSON parsing, a wind-chill calculation, and color terminal output. But every step is real I/O; he didn’t mock his own code.
I followed his approach, rebuilt it, and then reframed it around AI Agent / function calling / MCP tool scenarios. This post is basically my lab notes, and my answer to the question: “What language should you actually write an AI-callable tool in?”
What wx actually does
The tool is small but complete:
- Reads a city name from the command line.
- Calls a geocoding API for latitude and longitude.
- Calls a weather API for temperature, humidity, wind speed, and wind direction.
- Computes a feels-like temperature from the data.
- Prints the result with some color.
Abstract that flow and you get the standard chain every LLM uses when it calls an external tool:
LLM decides to call → pass args → HTTP → JSON → small compute → formatted result
The only difference is whether a human hits Enter or an Agent hits it for you.
The measured numbers first
Sarath’s test machine was an M3 MacBook Pro, 18 GB RAM, macOS 26. He used hyperfine :
hyperfine --warmup 3 --min-runs 50 './wx reykjavik'
Network calls hit a local mock server to avoid real-world jitter. The numbers came out like this:
| Language | Binary size | Compile / startup | Total runtime | Time to write |
|---|---|---|---|---|
| Zig 0.16 | 1.2 MB | 0.8 s | Fastest | 1 hour+ |
| Rust | 3.8 MB (needs LTO + strip) | 28 s first build | 4.8 ms | Medium |
| Go | 6.2 MB | <1 s | 5.2 ms | ~10 min |
| Bun | 45 MB (compiled) | run .ts directly | Near Rust | ~8 min |
| Node.js | ~100 MB runtime | run .ts directly | 82 ms cold start | ~8 min |
My first instinct was to root for Rust: 4.8 ms, binary squeezed to 3.8 MB, fast and small. Then I read the implementation details and changed my mind.
Rust: fast, but you pay a tax for “fast”
The Rust version uses clap, reqwest, serde, serde_json, and tokio — 95 lines total. The code itself isn’t complicated:
#[derive(Parser)]
#[command(name = "wx", about = "Weather lookup")]
struct Cli {
city: String,
}
#[derive(Deserialize)]
struct WeatherResponse {
current: CurrentWeather,
}
#[derive(Deserialize)]
struct CurrentWeather {
temperature_2m: f64,
wind_speed_10m: f64,
relative_humidity_2m: u8,
wind_direction_10m: f64,
}
What made me wince was the first compile. cargo build --release took 28 seconds. I actually thought cargo had frozen and switched to another terminal to check the process. Turns out reqwest + tokio drag in more than a hundred transitive dependencies, and every one of them has to compile.
The binary size is even more ironic. The default release build is 8.4 MB. To get it down to 3.8 MB you have to add this to Cargo.toml:
[profile.release]
lto = true
strip = true
That’s not a config a beginner knows. The first Rust CLI I ever shipped, I just ran release and never questioned 8.4 MB.
So Rust’s problem isn’t performance. It’s that before the tool is written, you pay a time tax.
Go: boring, but usable in ten minutes
The Go version is 68 lines with zero external dependencies. The struct looks like this:
type WeatherResponse struct {
Current struct {
Temperature float64 `json:"temperature_2m"`
WindSpeed float64 `json:"wind_speed_10m"`
Humidity int `json:"relative_humidity_2m"`
WindDir float64 `json:"wind_direction_10m"`
} `json:"current"`
}
func main() {
if len(os.Args) < 2 {
fmt.Fprintln(os.Stderr, "usage: wx <city>")
os.Exit(1)
}
city := os.Args[1]
// geocode, fetch weather, compute, print
}
Sarath said it took about ten minutes. My rebuild didn’t take more than fifteen. No dependencies to download, no compile to wait for. go build is instant.
Go’s old complaint shows up fast: in 68 lines you write five if err != nil { log.Fatal(err) } blocks. One for the request, one for reading the body, one for unmarshaling, then again for the second request. It’s not a bug, it’s just life.
resp, err := http.Get(url)
if err != nil { log.Fatal(err) }
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil { log.Fatal(err) }
Annoying? Yes. But it works, and cross-compiling is one command:
GOOS=linux go build
Rust can do it too, but you need to set up targets; Zig can do it better, but you need to know how. Go’s advantage is you don’t need to know. Zero config.
Zig: beautiful numbers, painful process
Zig’s numbers are the most seductive: 1.2 MB binary, 0.8 s compile, fastest execution. But the implementation took Sarath over an hour, 108 lines — the longest of the five.
const WeatherResponse = struct {
current: struct {
temperature_2m: f64,
wind_speed_10m: f64,
relative_humidity_2m: u8,
wind_direction_10m: f64,
},
};
pub fn main() !void {
var debug_allocator = std.heap.DebugAllocator(.{}){};
defer _ = debug_allocator.deinit();
const allocator = debug_allocator.allocator();
// every function that might allocate needs this allocator passed in
}
That looks fine, but the real problem was TLS. Zig’s std.http.Client implements TLS itself, but it couldn’t find the system CA bundle and just printed:
error.TlsInitializationFailed
One line, no extra context. Sarath spent an hour piecing together a fix from GitHub issues: manually load certificates with std.crypto.Certificate.Bundle and hand them to the client.
Another thing you do over and over in Zig: pass an allocator to every function that might allocate on the heap. For a weather CLI that allocates maybe a handful of strings, you still thread the allocator through everywhere. The author put it well: “like putting on a seatbelt to walk to the mailbox.”
One more detail: ReleaseSmall builds have no stack traces. That 1.2 MB binary is gorgeous, but you’ll miss ReleaseSafe when you’re debugging.
I have mixed feelings about Zig. The language design is genuinely beautiful, and 1.2 MB binaries are tempting. But in 2026 its ecosystem isn’t ready for a “weekend CLI.”
Bun: the most fun to write, the most painful to ship
The Bun version is 47 lines, the shortest of the five. Fetch is built in, JSON parses in one line, top-level await works out of the box:
const city = Bun.argv[2];
if (!city) {
console.error("usage: wx <city>");
process.exit(1);
}
const geoRes = await fetch(
`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(city)}&count=1`
);
const geo = await geoRes.json();
const { latitude, longitude } = geo.results[0];
const wxRes = await fetch(
`https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t=temperature_2m,wind_speed_10m,relative_humidity_2m,wind_direction_10m`
);
const data = await wxRes.json();
When I wrote this version, most of my time wasn’t spent on HTTP — it was on the wind-chill formula. Bun hides all the infrastructure.
But shipping ruins everything:
bun build --compile
The artifact is 45 MB, because Bun bundles the JavaScriptCore engine and the whole runtime. If you’re writing for yourself and running the .ts file directly, it’s perfect. If you’re asking users to download a single executable, 45 MB is a hard sell.
Node.js: same code as Bun, slower runtime
The Node.js code is almost identical to Bun; the difference is runtime. I ran it with Node.js 24 LTS:
node wx.ts
TypeScript is now natively supported (erasable syntax only), so no tsx needed. But cold start is slow: an empty script takes 82 ms, while Bun takes 24 ms. A full run is roughly an order of magnitude slower than Bun.
For distribution, Node.js either requires a ~100 MB runtime install or the experimental Single Executable Application (SEA) feature. SEA artifacts in 2026 are still experimental and end up similar in size to Bun’s compiled output.
My conclusion matches Sarath’s: starting a new Node.js CLI tool in 2026 is just choosing a worse Bun.
Reframed for AI tool calling
Now back to the AI Agent scenario. A tool an LLM can call is essentially this wx pattern scaled up:
- Receive args → call an external API → parse JSON → do a small calculation → return structured data
For this workload I re-scored the five languages:
| Language | Tool-calling fit | Performance | Dev experience | Distribution | Overall |
|---|---|---|---|---|---|
| Go | ★★★★★ | ★★★★☆ | ★★★★★ | ★★★★★ | First choice |
| Rust | ★★★★★ | ★★★★★ | ★★★☆☆ | ★★★★☆ | Pick when maintenance matters |
| Bun | ★★★★★ | ★★★★☆ | ★★★★★ | ★★☆☆☆ | Internal / personal tools |
| Zig | ★★★☆☆ | ★★★★★ | ★★☆☆☆ | ★★★★★ | Wait and watch |
| Node.js | ★★★★☆ | ★★☆☆☆ | ★★★★☆ | ★★☆☆☆ | Not for new projects |
Why is Go the best overall?
It doesn’t win any single category, but it minimizes total friction: the standard library has HTTP and JSON, compile time is under a second, you ship one binary, cross-compilation is one command, and the performance gap versus Rust is under 1 ms. For AI tool calling — small I/O plus small compute — Go’s marginal gains land exactly right.
A concrete example: today you want to give your Agent a tool that checks a GitHub repo’s star count. In Go, from main.go to a gh-stars binary, maybe 20 minutes. In Rust, just waiting for dependency compilation is long enough to finish half a coffee.
When to pick Rust?
When the tool is long-lived, publicly distributed, and cannot fail. Think MCP servers, code-analysis tools, security-sensitive tools. Rust’s compile-time tax buys runtime stability.
What about Bun?
If the tool only runs on your machine or inside your team, Bun is the happiest path. The moment you need to distribute it, the 45 MB starting size becomes a real problem.
Zig and Node.js
Watch Zig until 1.0. For new Node.js tools, just skip it — Bun does the same job better.
My final recommendation
- Want to ship an AI-callable tool quickly? Pick Go.
- Building something large, long-lived, and zero-tolerance on stability? Pick Rust.
- Internal script that stays on your machine? Pick Bun.
- Starting a brand-new Node.js CLI tool in 2026? Not necessary.
- Zig? Keep following it, but it’s not a production language yet.
Rust is fast and safe. But in the specific scenario of “tool calling,” it lost to the more “boring” Go. I didn’t expect that either — after all, three years ago we were saying “Rust is the future of CLI.”
Source:
- Sarath S, “Rust vs Go vs Zig vs Bun vs Node.js for CLI tools in 2026: I built the same tool five times”, Medium, Aug 2026.
