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.
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:
- 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.
- Startup time. A 500ms startup penalty for a tool you invoke hundreds of times a day adds up.
- 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:
| Criteria | Go | Rust |
|---|---|---|
| Compile speed | ★★★★★ | ★★☆☆☆ |
| Binary size | ★★★★☆ | ★★★★★ |
| Runtime performance | ★★★★☆ | ★★★★★ |
| Error handling | Verbose but clear | Verbose but composable |
| CLI libraries | Cobra (excellent) | Clap (excellent) |
| Learning curve | Gentle | Steep |
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 textserdemeans 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.
Stringvs&strvsOsStringvsPathBuf— 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 | tarinstall - 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.