Every quarter, a founder asks us the same question in a slightly different costume. Sometimes it is "should our new matching engine be in Rust?" Sometimes it is "we have a Node monolith and we want to break out a hot path, what do we use?" Sometimes it is just "Rust or Go?" said with the resigned tone of a person who has read too many threads on Hacker News and now wants an adult to tell them what to do.
We have shipped production systems in both for years. We have rewritten Go in Rust and we have rewritten Rust in Go, and in each case the rewrite was the right call at the time. So here is the rubric we actually use, with the trade-offs that matter and the ones that do not.
Why this question keeps coming up
The languages are close enough in 2026 that the choice feels reversible, and far enough apart that getting it wrong is expensive. Both compile to fast native binaries. Both have first-class concurrency stories. Both have respectable tooling and respectable package ecosystems. Neither will embarrass you in front of an investor.
The reason the question keeps coming up is that the marketing for each language has converged on the same promise: safe, fast, modern. The reality is that they are optimised for different failure modes. Rust is optimised for the bug you cannot see until production. Go is optimised for the engineer you have not hired yet. Pick the wrong optimisation for your stage and you will feel it, but not for six months.
The two honest use cases for Rust
We use Rust when at least one of two things is true. First, when a bug in the code is unrecoverable in the business sense: consensus, settlement, signature verification, contract parsing, anything where wrong output is worse than no output. Second, when the system has a hard latency budget that does not tolerate garbage collector pauses, typically anything advertising single-digit-millisecond P99.
Outside those two cases, Rust is a tax. The borrow checker is genuinely helpful, the type system is genuinely expressive, and the compile times are genuinely painful. We are happy to pay the tax when the alternative is paying a customer back. We are less happy to pay it for an internal admin tool that runs twice a day.
The shape of the code we write in Rust is usually narrow and deep. A consensus loop. A trade matching engine. A WASM contract executor. The surface area is small, the invariants are dense, and the test suite reads like a small proof. The team writing it is two or three senior engineers who already know the language. That combination is where Rust earns its keep.
The two honest use cases for Go
We use Go when at least one of two different things is true. First, when the system is a network service with reasonable but not extreme latency requirements: APIs, gateways, webhook receivers, background workers, the unglamorous middle of any product. Second, when the team is going to grow, or has already grown past the point where everyone can hold the whole codebase in their head, and the cost of onboarding a new engineer is real.
Go is the language we reach for when the value is in shipping the service, not in the service itself. Most products live there. A founder's job in the first two years is usually to find out whether the product is needed at all, and Go gets you to that answer with fewer detours. The standard library is enough. The error handling is verbose but explicit. The build is fast. The deploys are boring. Nothing about the language is going to surprise an engineer you hire next month.
The shape of the code we write in Go is wide and shallow. Many handlers, many endpoints, many goroutines, many small files that each do one thing. The surface area is large, the invariants are loose, and the test suite leans on integration. Go takes the shape of the team, not the other way around.
The grey zone
The honest answer is that maybe half the systems we are asked to evaluate sit in a grey zone where both languages would work, and the decision should be made on team and timeline rather than on language properties. This is the part of the conversation most threads on the internet skip.
Here is the comparison we keep on a sticky note for the grey zone calls.
| Dimension | Rust | Go |
|---|---|---|
| Latency ceiling | Sub-millisecond, predictable | Low ms, GC pauses possible |
| Memory footprint | Minimal, zero GC overhead | Low, GC adds ~10-20% overhead |
| Compile time | Slow (1-5 min large projects) | Fast (seconds) |
| Hire-ability | Small senior pool | Larger pool, growing fast |
| AI coding fit | High (borrow checker guides LLMs) | High (simple syntax, fast feedback) |
| Best for | Correctness-critical, throughput-critical | APIs, services, CLIs, glue code |
A note on the AI coding row, because we get asked about it often. Both languages are good targets for AI-assisted work, but for opposite reasons. Rust gives the model an unforgiving compiler that catches most plausible-looking nonsense before a human sees it. Go gives the model a small, consistent surface area where a fast feedback loop catches the rest. We have written more about that calculation in our notes on vibe coding. The short version: language choice does not get you out of the discipline layer.
Real project patterns
To make this less abstract, here is a worker pattern we write often in both languages. Pull jobs off a queue, process them concurrently with bounded parallelism, handle shutdown cleanly. This is the kind of code that exists somewhere in almost every backend we have shipped.
worker.rs (tokio)use tokio::sync::mpsc; use tokio::task::JoinSet; pub struct Job { pub id: u64, pub payload: Vec<u8> } pub async fn run( mut jobs: mpsc::Receiver<Job>, concurrency: usize, ) -> anyhow::Result<()> { let mut set = JoinSet::new(); let mut in_flight = 0usize; while let Some(job) = jobs.recv().await { while in_flight >= concurrency { if let Some(res) = set.join_next().await { in_flight -= 1; if let Err(e) = res? { tracing::error!(?e, "job failed"); } } } set.spawn(async move { process(job).await }); in_flight += 1; } while let Some(res) = set.join_next().await { if let Err(e) = res? { tracing::error!(?e, "job failed"); } } Ok(()) } async fn process(job: Job) -> anyhow::Result<()> { // ... real work here Ok(()) }
worker.gopackage worker import ( "context" "log/slog" "sync" ) type Job struct { ID uint64 Payload []byte } func Run(ctx context.Context, jobs <-chan Job, concurrency int) error { sem := make(chan struct{}, concurrency) var wg sync.WaitGroup for { select { case <-ctx.Done(): wg.Wait() return ctx.Err() case job, ok := <-jobs: if !ok { wg.Wait() return nil } sem <- struct{}{} wg.Add(1) go func(j Job) { defer wg.Done() defer func() { <-sem }() if err := process(ctx, j); err != nil { slog.Error("job failed", "id", j.ID, "err", err) } }(job) } } } func process(ctx context.Context, j Job) error { // ... real work here return nil }
Both versions do the same job. The Rust version is more explicit about errors and ownership, and the compiler will refuse to build it if the lifetimes do not line up. The Go version reads top to bottom, uses primitives a new hire will recognise on day one, and would be running in production by lunch. Neither is better in the abstract. The right one is the one your team can extend, debug, and keep boring for the next two years.
A few concrete patterns from our recent work that map onto this rubric. A consensus client we built went into Rust the moment we wrote the threat model: a bug in finality logic is unrecoverable, the latency budget was tight, and the team writing it was three people who already knew the language. A wallet-as-a-service API went into Go: the hot paths were database round-trips and HTTP calls, the team was going to grow, and the value was in shipping the integrations. A CLI tool for the same client, which engineers run on their laptops, also went into Go, because Go ships single-binary CLIs better than anything else and we did not need the safety guarantees for a developer tool.
The decision flowchart
If we had to compress the rubric into a single diagram, here is what it would look like. We use this as a starting point in scoping calls, not as a rule.
Is a bug here unrecoverable? (consensus, settlement, parsing)
├── YES → Rust
└── NO: Is P99 latency under 5ms required?
├── YES → Rust
└── NO: Is the team under 5 engineers?
├── YES → Go
└── NO: Does this domain need rapid iteration?
├── YES → Go
└── NO → Either works. Go ships faster.
The last branch is the one most teams get wrong. When both languages would work, the right move is almost always Go, because the second-order costs of Rust (compile times, smaller hiring pool, slower iteration on uncertain requirements) compound in ways that do not show up in a side-by-side benchmark. We have watched more than one team adopt Rust for the prestige of it and then quietly migrate the non-critical services back to Go a year later, after the first three senior engineers spent half their time waiting for builds.
The other branch worth a comment is the "team under 5 engineers" one. We do not mean that small teams cannot ship Rust; plenty do, including us. We mean that a small team cannot afford to ship Rust for everything. The right pattern for a small team is to write the unrecoverable parts in Rust, the rest in Go, and accept that you now have two languages in the repo. That is fine. Polyglot is cheaper than rewriting.
What we would tell our past selves
Five years ago, we picked Rust for a few systems where we should have picked Go. The systems worked. They are still in production. But the cost of keeping them alive, training new engineers on them, and shipping changes against them is higher than it needed to be, and the safety we bought was not the bottleneck on those products.
Three years ago, we picked Go for a settlement path we should have written in Rust. We shipped on time. Then we spent six months patching subtle concurrency bugs and one weekend in a war room over a race that lost a customer real money. The bug was the kind of bug Rust would have refused to compile. We rewrote that service in Rust the next quarter and have not touched it since.
The lesson, both times, was the same. The language is not the product. The language is the cost of the product. The job is to pick the language that minimises the cost over the system's actual lifetime, not the language that wins the most points on a benchmark or signals the most engineering seriousness on a careers page.
If you are picking a language for a new system right now, start from the failure mode you cannot survive, not from the language you would prefer to write. If a bug is recoverable and the latency is reasonable, write it in Go and use the time you saved to think about the product. If a bug is unrecoverable or the latency is tight, write it in Rust and use the friction the language imposes as a feature, not a cost.
If you are not sure which side of that line your system falls on, that is a conversation we are happy to have. We do not sell language consulting. We build software, and we have made enough of these calls, in both directions, to talk through yours without preaching.