Distributed Systems

Go vs Rust for Microservices: When to Choose Which

Go vs Rust for microservices: use Go by default, reach for Rust only on hot paths where tail latency or memory is a measured need. The decision framework.

Part of Polyglot Microservices: Choosing the Right Language
Go vs Rust for microservices, shown as a data path forking into a cyan Go branch and an amber Rust branch

When you compare Go vs Rust for microservices, the honest answer is that they are not really competitors. Use Go for almost every microservice you build, and reach for Rust only on the hot paths where tail latency or memory footprint is a hard requirement you can measure. That single rule resolves most of the debate.

Go wins on velocity, hiring, and a cloud-native ecosystem it already owns. Rust wins when garbage-collection pauses or RAM-per-pod start showing up in your latency SLOs and your infrastructure bill. Everything below is the framework for telling those two situations apart before you commit a team to a rewrite you cannot easily undo.

Why the language choice matters

Picking the wrong language for a service is one of the few architecture mistakes you cannot refactor your way out of cheaply. A rewrite is a quarter of someone’s life.

Most “Go vs Rust” debates online are tribal. They benchmark a tight numeric loop, declare a winner, and tell you nothing about a service that spends 95% of its wall-clock time waiting on Postgres and one downstream call.

This is the decision I actually make when a new service shows up in design review, and the production signals that move me off the Go default. This post is part of the Language choices in polyglot microservices series.

Go vs Rust at a glance

Before the detail, here is the comparison most teams actually need. Treat it as a starting map, not a verdict; your workload decides.

DimensionGoRust
Best fitAPIs, control planes, cloud-native servicesProxies, runtimes, latency-critical hot paths
Compile speedSingle-digit secondsOften an order of magnitude slower
Memory modelGarbage collectedOwnership, no GC
Tail latencyExcellent average, occasional GC pauseDeterministic, no collector pauses
Memory footprintLow hundreds of MB typicalTens of MB typical
ConcurrencyGoroutines, simpleAsync with Send/Sync, powerful but strict
Hiring/onboardingDays, large talent poolWeeks to months, smaller pool
EcosystemOwns cloud-native (k8s, Docker, etc.)Owns systems-level and Wasm

The 2026 consensus: systems versus services

The industry argument has mostly ended, and it ended in a truce. Rust is for code that other software runs on. Go is for code that runs on existing infrastructure.

That single distinction predicts most real decisions. Kernels, drivers, proxies, runtimes, and WebAssembly targets go to Rust. APIs, microservices, control planes, and cloud-native tooling go to Go.

The case studies people cite back this up, and they are usually misread. Discord rewrote exactly one service, “Read States,” because Go’s garbage collector caused latency spikes while scanning a very large in-memory cache holding millions of entries. Most of Discord’s backend stayed in Go, and they said so in the original post.

Cloudflare’s Pingora proxy replaced an NGINX-and-C codebase, not a Go one. Cloudflare runs an enormous amount of Go in production. Rust is replacing C and C++ in performance-critical systems work, not displacing Go from service work.

Why Go is the right default for microservices

Go is the right default for microservices because its advantages are boring, and boring is exactly what you want underneath a hundred services.

Compile times stay in single-digit seconds even on large modules. That number sets the speed of your entire inner loop: edit, build, test, deploy. Rust’s borrow checker and monomorphization buy you safety and runtime speed at the cost of build times that are routinely an order of magnitude longer. You pay that tax every single day.

The standard library covers HTTP, JSON, crypto, and context cancellation without pulling a dependency tree. A new engineer is productive in days, not the months Rust’s ownership model can demand before someone stops fighting the compiler.

Goroutines make concurrency legible. A blocking call inside a goroutine reads like straight-line code, and the scheduler handles the rest. Rust’s async story is more powerful and far less forgiving: Send/Sync bounds, pinning, and runtime selection are real cognitive load for a team that just wants to fan out twenty requests and collect the results.

Then there is the ecosystem fact that settles most arguments. Kubernetes, Docker, Terraform, Prometheus, Helm, and etcd are all written in Go. When your service lives inside that world, Go’s client libraries, instrumentation, and idioms are first-class, and everyone else’s are ports.

Is Go’s garbage collector a problem in production?

Rarely. Modern Go’s garbage collector delivers sub-millisecond pauses on multi-gigabyte heaps for ordinary workloads, so on a long-running service doing normal work you will not feel it. The objection is mostly stale.

You will not feel it on your CRUD API, your control plane, or your typical request handler. The collector has had years of production hardening. The GC becomes a real problem in one specific shape, covered next.

When should you use Rust instead of Go?

Use Rust instead of Go when you have a measured, hard requirement that Go’s runtime cannot meet: a strict tail-latency SLO, a large GC-scanned in-memory structure, a fleet-scale memory cost, or genuine systems-level work. Outside those cases, Go is the better choice.

The Go default breaks in a small number of places, and they are identifiable before you write a line of code.

A large, long-lived in-memory structure that the GC must walk. This is the Discord “Read States” shape exactly. Millions of live objects mean the collector spends real time on every cycle, and you get periodic latency spikes that no amount of GOGC tuning fully removes.

A hard tail-latency SLO, the kind where p99.9 is a contractual or user-perceptible number. Go’s GC gives you excellent averages and the occasional pause you did not schedule. Rust gives you deterministic cleanup: memory and resources are freed at points you can read off the code, with no collector deciding when.

Memory footprint at fleet scale. A Rust service commonly holds a fraction of the RAM of an equivalent Go service, often tens of megabytes versus low hundreds. On one pod that difference is noise. Across a thousand pods it is a budget line item. (Measure your own services; the ratio depends heavily on workload.)

Genuine systems work, such as a proxy, a storage engine, or a custom load generator: anything where you are the infrastructure other code runs on. That is Rust’s home turf, and Go is the wrong tool.

The Go vs Rust decision matrix I actually use

Run a candidate service through these questions in order. The first “yes” that lands on Rust is your answer. If none do, it is Go.

  1. Is this systems-level code that other services run on? Proxy, runtime, storage engine, agent, Wasm target. Yes means Rust.
  2. Is there a hard p99.9 latency SLO that a GC pause would violate? Not “we’d like it fast,” but a number someone signs off on. Yes means Rust is a candidate.
  3. Does the service hold a large, long-lived in-memory structure the GC must scan? Multi-million-entry caches, big in-process indexes. Yes means Rust is a candidate.
  4. Is per-pod memory a real cost driver at your replica count? Do the arithmetic: footprint delta times replicas times price. If it clears a threshold you care about, Rust is a candidate.
  5. Everything else. Go.

The trap is answering question 2 or 3 with ambition instead of evidence. “We might need low latency someday” is not a yes. A dashboard showing GC-correlated spikes against your SLO is a yes.

The other trap is ignoring the team. A five-person crew with no Rust experience will ship a worse Rust service than a good Go one, and they will ship it a quarter late. Language choice is a staffing decision as much as a technical one.

The hybrid pattern most large systems converge on

You do not have to choose one language for the whole system. The pattern that keeps winning is Go for the network and orchestration layer, Rust for the compute-intensive core.

A request lands on a Go edge service that handles auth, validation, routing, and fan-out, because that code changes often and benefits from velocity. The one stage that is CPU-bound or latency-critical is a Rust service the Go layer calls over gRPC.

This keeps roughly 80% of your code in the language that is fast to change and 20% in the language that is fast to run. It also contains the Rust blast radius: only the engineers who need to touch the hot path pay the borrow-checker tax.

The cost is a language boundary, and language boundaries are not free. Serialization, error-model mismatches, and cross-language deadline propagation all become real concerns the moment a Go service calls a Rust one. I cover exactly how those boundaries break in Why Language Boundaries Break Polyglot Microservices, and how to make gRPC behave across runtimes in gRPC Across Languages: Production Lessons.

If you do commit to a Rust hot path, the operational details (panic strategy, allocator choice, async runtime) matter more than the language win itself. That is its own post: Building Rust Hot Path Services in Production.

Is Rust faster than Go for microservices?

On synthetic CPU benchmarks, yes, usually by 15 to 30 percent. On a real I/O-bound microservice the two are often indistinguishable, because the bottleneck is the database, the cache, and the network, not the language. Rust’s reliable edge is tail latency and memory, not average throughput.

Your microservice is not CPU-bound. It spends its time waiting on a database query, a cache lookup, a downstream RPC, and the network. In that regime, Go’s optimized runtime and GC routinely match Rust on real HTTP throughput, because neither language is the bottleneck.

So measure the thing that actually bites: tail latency under sustained load, and memory under a realistic working set. Throughput on an empty handler tells you which language has a faster for loop, not which one you should run in production. Before any rewrite, capture a latency budget for the hot path: how many milliseconds are spent in the network, the database, the downstream RPC, and in-service compute. If compute is a small slice, a language change will not save you.

A production checklist before you write Rust

Before you greenlight a Rust service inside a mostly-Go system, get a yes on all of these. If any is no, the honest answer is probably Go.

  • There is a written SLO or cost number the current Go service is missing, with a dashboard that shows it.
  • At least two engineers can review Rust competently, not just write it.
  • The service boundary is stable, so you are not redesigning the contract while also learning the borrow checker.
  • Build and CI for Rust are already wired into your repo, so you are not bootstrapping toolchain and service at once.
  • You have decided the panic, allocator, and async-runtime defaults up front, not per pull request.

That last point is where most first Rust services wobble. The language choice is the easy part. Running it well is the work.

What I’d do differently

The mistake I have watched teams make is reaching for Rust on a service that felt performance-critical without a number proving it. The result is a slower team, a longer schedule, and a p99 a well-tuned Go service would have matched.

If I were starting a polyglot system today, I would write everything in Go, instrument tail latency and per-pod memory from day one, and let the dashboards nominate Rust candidates. The data, not the debate, picks the language.

The inverse mistake is rarer but real: staying on Go for a genuine systems component because the team likes Go, and paying for it in latency spikes forever. When the signal is unambiguous, move. Just make sure it is a signal and not a vibe.

Sources

Frequently asked questions

Is Rust faster than Go for microservices?

On synthetic CPU benchmarks, yes, usually by 15 to 30 percent. On real I/O-bound microservices the two are often indistinguishable, because the bottleneck is the database and the network, not the language. Rust's reliable advantage is tail latency and memory footprint, not average throughput.

Should I rewrite my Go services in Rust?

Almost never wholesale. Rewrite a single service only when a dashboard shows a GC-correlated latency problem or a memory cost you can quantify. Discord rewrote one service, not its backend, and that is the right model.

Is Go's garbage collector a problem in production?

Rarely. Modern Go GC delivers sub-millisecond pauses on multi-gigabyte heaps for ordinary workloads. It becomes a problem mainly when a service holds a large, long-lived in-memory structure the collector must scan on every cycle.

Can I mix Go and Rust in the same system?

Yes, and most large systems do. The common pattern is Go for the network and orchestration layer, Rust for the compute-intensive core, connected over gRPC. The cost is a language boundary you have to manage deliberately.

Which is better for hiring, Go or Rust?

Go, by a wide margin. The talent pool is larger, onboarding takes days instead of months, and most cloud-native engineers already know it. Rust hiring is viable but slower, which is itself a reason to keep the Rust surface small.

Newsletter

Liked this breakdown?

Production wisdom on distributed systems, delivered when there is something worth saying. No spam, unsubscribe anytime.