After 18 months of building production services in both Go and Rust, I have strong opinions about when to choose each language. This isn't another benchmark comparison or syntax tutorial. This is the brutally honest truth about building real systems with real constraints.
Spoiler: the answer isn't what you think, and it has nothing to do with performance benchmarks.
The Context That Changes Everything
I'm building NextDeploy, a deployment platform that needs to:
Both Go and Rust could handle these requirements. The question was: which would let us ship faster and sleep better?
Why I Initially Chose Go
Developer Velocity
Go's simplicity is its superpower. New team members can contribute meaningfully within days, not weeks. The language has one way to do most things, which eliminates bikeshedding and reduces cognitive load.
// Go: Simple and readable
func processDeployment(ctx context.Context, req DeploymentRequest) error {
build, err := buildImage(ctx, req.RepoURL)
if err != nil {
return fmt.Errorf("build failed: %w", err)
}
if err := deployContainer(ctx, build.ImageID); err != nil {
return fmt.Errorf("deploy failed: %w", err)
}
return nil
}
Compare this to the equivalent Rust code:
// Rust: More explicit, more complex
async fn process_deployment(
ctx: Context,
req: DeploymentRequest,
) -> Result<(), DeploymentError> {
let build = build_image(&ctx, &req.repo_url)
.await
.map_err(DeploymentError::BuildFailed)?;
deploy_container(&ctx, &build.image_id)
.await
.map_err(DeploymentError::DeployFailed)?;
Ok(())
}
The Rust version is more explicit about error handling and async behavior, but it's also more verbose and requires more mental overhead.
Ecosystem Maturity
Go's ecosystem for backend services is incredibly mature. Need a web framework? Gin or Echo. Database ORM? GORM. Message queues? Go has excellent libraries for everything.
Rust's ecosystem is growing rapidly, but it's still catching up. Finding the "blessed" way to do common tasks often requires research and experimentation.
Operational Simplicity
Go produces single binaries with no runtime dependencies. Deployment is as simple as copying a file. Docker images are tiny. Memory usage is predictable.
# Go: Simple deployment
FROM scratch
COPY nextdeploy /
ENTRYPOINT ["/nextdeploy"]
This operational simplicity saved us countless hours in deployment and debugging.
Where Go Started to Hurt
Memory Management
Go's garbage collector is excellent, but it's still a garbage collector. For our build service, which processes large Docker contexts, GC pauses became noticeable under load.
We had builds that would pause for 50-100ms during GC cycles. For most applications, this is fine. For a system that needs to feel instant, it was death by a thousand cuts.
Error Handling Verbosity
Go's explicit error handling is a feature, not a bug. But after writing thousands of lines of error handling code, the verbosity becomes exhausting:
func complexOperation() error {
result1, err := step1()
if err != nil {
return fmt.Errorf("step1 failed: %w", err)
}
result2, err := step2(result1)
if err != nil {
return fmt.Errorf("step2 failed: %w", err)
}
result3, err := step3(result2)
if err != nil {
return fmt.Errorf("step3 failed: %w", err)
}
return nil
}
This pattern repeated hundreds of times across our codebase. The signal-to-noise ratio started to suffer.
Performance Ceiling
Go is fast enough for most use cases, but it has a performance ceiling. When we needed to process thousands of Docker layers per second, Go's runtime overhead became the bottleneck.
The Rust Experiment
Six months into building NextDeploy, I rewrote our build service in Rust. Here's what I learned:
Performance is Incredible
The Rust version was 3-4x faster than the Go version for CPU-intensive tasks. More importantly, it had predictable performance with no GC pauses.
// Rust: Zero-cost abstractions in action
fn process_layers(layers: &[Layer]) -> Result<ProcessedLayers, Error> {
layers
.par_iter() // Parallel processing with rayon
.map(|layer| process_layer(layer))
.collect::<Result<Vec<_>, _>>()
.map(ProcessedLayers::new)
}
The parallel processing capabilities with Rayon made complex operations trivial to optimize.
Memory Safety Without Runtime Cost
Rust's ownership system caught bugs that would have been runtime panics in Go:
// This won't compile - Rust prevents data races at compile time
fn broken_example() {
let mut data = vec![1, 2, 3];
let reference = &data[0];
data.push(4); // Error: cannot borrow as mutable
println!("{}", reference);
}
This compile-time safety eliminated entire classes of bugs that plagued our Go services.
The Learning Curve is Real
It took our team 3 months to become productive in Rust. The borrow checker is unforgiving, and the type system requires a different way of thinking about problems.
We had senior engineers struggling with concepts that would be trivial in other languages. The productivity hit was significant.
The Honest Comparison
Choose Go When:
Choose Rust When:
What I Actually Did
Here's the plot twist: I kept both.
Our architecture now uses Go for the API layer and coordination services, and Rust for the performance-critical build engine. This hybrid approach gives us the best of both worlds:
// Go service calls Rust service via gRPC
func (s *APIServer) handleBuild(c *gin.Context) {
req := &buildpb.BuildRequest{
RepoUrl: c.PostForm("repo_url"),
Branch: c.PostForm("branch"),
}
resp, err := s.buildClient.Build(c.Request.Context(), req)
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
c.JSON(200, gin.H{"build_id": resp.BuildId})
}
The Metrics That Matter
After 12 months of running this hybrid architecture:
Lessons Learned
Language Choice is a Business Decision
The "best" language is the one that helps your team ship reliable software quickly. Technical superiority doesn't matter if it slows down your business.
Polyglot Architectures Work
You don't have to choose one language for everything. Use the right tool for each job, and invest in good interfaces between components.
Team Skills Matter More Than Language Features
A great Go developer will build better systems than a mediocre Rust developer, regardless of language capabilities.
Performance Optimization is Iterative
Start with the simpler language and optimize bottlenecks as you find them. Premature optimization is still the root of all evil.
The Uncomfortable Truth
Most applications don't need Rust's performance. Most teams can't afford Rust's learning curve. Most businesses prioritize shipping over perfection.
Go is the pragmatic choice for most backend services. Rust is the right choice when Go isn't fast enough.
The uncomfortable truth is that language choice matters less than we think. Good architecture, clear interfaces, and solid engineering practices matter more than syntax and performance benchmarks.
What I'd Tell My Past Self
If I could go back 18 months, I'd still choose Go first. But I'd plan for the hybrid architecture from the beginning:
1. **Start with Go** for rapid prototyping and business logic
2. **Design clean interfaces** between components
3. **Identify performance bottlenecks** through measurement, not assumption
4. **Rewrite critical paths in Rust** when Go becomes the bottleneck
5. **Invest in tooling** to make the polyglot architecture manageable
The Real Winner
The real winner isn't Go or Rust. It's having the flexibility to choose the right tool for each problem.
Build systems that can evolve. Design interfaces that can survive implementation changes. Focus on solving customer problems, not language wars.
Your users don't care what language you use. They care that your software works reliably and performs well. Everything else is just implementation details.
---
*What's your experience with Go vs Rust in production? I'd love to hear your stories—the good, the bad, and the ugly. Reach out to me at yussuf@hersi.dev or on Twitter [@yussufhersi](https://twitter.com/yussufhersi).*