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:

  1. Reads a city name from the command line.
  2. Calls a geocoding API for latitude and longitude.
  3. Calls a weather API for temperature, humidity, wind speed, and wind direction.
  4. Computes a feels-like temperature from the data.
  5. 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:

LanguageBinary sizeCompile / startupTotal runtimeTime to write
Zig 0.161.2 MB0.8 sFastest1 hour+
Rust3.8 MB (needs LTO + strip)28 s first build4.8 msMedium
Go6.2 MB<1 s5.2 ms~10 min
Bun45 MB (compiled)run .ts directlyNear Rust~8 min
Node.js~100 MB runtimerun .ts directly82 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}&current=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:

LanguageTool-calling fitPerformanceDev experienceDistributionOverall
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.