Why We Chose Rust for Our CLI Tools

A practical comparison of building CLI tools in Rust versus Go and Python — performance, developer experience, and the tradeoffs we encountered.

Sanket Jawali
Sanket Jawali @SanketJawali

We recently rewrote three internal CLI tools from Python to Rust. This isn’t a “Rust is the best language” post — it’s an honest account of what worked, what didn’t, and why we’d make the same choice again.

The Problem

Our Python CLIs worked fine functionally. But as the team grew, three issues became impossible to ignore:

  1. Distribution pain. Shipping a Python script means shipping a Python environment. Virtual envs, dependency conflicts, version mismatches — all real problems for a team of 40.
  2. Startup time. A 500ms startup penalty for a tool you invoke hundreds of times a day adds up.
  3. No type safety. Refactoring CLI argument parsing was a minefield. Change a flag name? Hope you found every usage.

Why Not Go?

Go was our second contender. Honest comparison:

CriteriaGoRust
Compile speed★★★★★★★☆☆☆
Binary size★★★★☆★★★★★
Runtime performance★★★★☆★★★★★
Error handlingVerbose but clearVerbose but composable
CLI librariesCobra (excellent)Clap (excellent)
Learning curveGentleSteep

Go would have been a perfectly fine choice. We went with Rust because:

  • clap’s derive macros make CLI argument definitions a dream — they’re declarative, type-safe, and auto-generate help text
  • serde means we can deserialize config files with zero boilerplate
  • Pattern matching on error types made our error handling more precise
use clap::Parser;

#[derive(Parser)]
#[command(name = "deploy", about = "Deploy services to staging/prod")]
struct Cli {
    /// Target environment
    #[arg(short, long)]
    env: Environment,

    /// Services to deploy
    #[arg(required = true)]
    services: Vec<String>,

    /// Skip confirmation prompt
    #[arg(long, default_value_t = false)]
    yes: bool,
}

That struct is the entire CLI interface. Types enforced at compile time. Help text generated automatically.

What Hurt

It wasn’t all smooth:

  • Compile times are real. A clean build takes 45 seconds. Incremental builds are fast, but CI pipelines feel it.
  • The learning curve hit the team unevenly. Engineers with C/C++ backgrounds adapted in days. Those from Python/JS backgrounds needed weeks to be comfortable with lifetimes and borrowing.
  • String handling requires adjustment. String vs &str vs OsString vs PathBuf — there’s a reason for each, but it’s jarring at first.

The Results

After three months with the Rust versions in production:

  • Startup time: 500ms → 8ms
  • Binary distribution: Single static binary, curl | tar install
  • Refactoring confidence: Dramatically higher. The compiler catches everything.
  • Team satisfaction: High, after the initial learning curve

The CLIs feel instant now. There’s something satisfying about a tool that responds before your finger lifts from the Enter key.


Would we use Rust for everything? No. For quick scripts, Python is still unbeatable. For web services, the ecosystem isn’t there yet for our needs. But for CLI tools that ship to humans and need to feel fast? It’s hard to argue with the results.