Imagine: a Go service handles 10,000 requests per second, but under peak load it suddenly crashes with out of memory. The developer added a goroutine for each request — elegant, but without limiting concurrency. Sound familiar? This is a classic situation that marks the beginning of the journey into modern systems programming. Go and Rust are often pitted against each other, but in practice they solve different problems. Let's break down how this works, where people go wrong, and what you need to know before choosing a tool.
Why Go and Rust, not just one
Go was created for simplicity and development speed. Its runtime takes care of goroutine management, garbage collection, and network I/O. You write straightforward code, and the scheduler distributes goroutines across OS threads. This is convenient for microservices, APIs, and backends where throughput and low latency matter with a large number of concurrent connections.
Rust solves a different problem — memory safety without a garbage collector. The ownership and borrowing system is checked at compile time, so there are no GC pauses or data races at runtime. This is critical for utilities, drivers, WebAssembly modules, and anything where predictability matters more than coding speed.
The key difference: Go optimizes developer time, Rust optimizes runtime and reliability. Choosing between them is not a religion but an engineering trade-off.
Task breakdown: a caching proxy in Go
Let's take a typical task — an HTTP proxy that caches responses from an external API. In Go, this takes a few dozen lines: http.Server, sync.Map for the cache, goroutines for handling requests. But the devil is in the details.
Step 1. Limiting concurrency. If you spawn a goroutine for each request without a limit, at 10,000 RPS and a backend response time of 100 ms, the number of concurrent goroutines will reach 1,000. Each takes a stack of at least 2 KB (initial size, can grow). That's 2 MB total — not much. But if the handler makes a database query with a 5-second timeout, the number of goroutines will grow to 50,000, and memory to 100 MB or more. The fix is a worker pool or a semaphore on a channel.
Step 2. Cache with TTL. sync.Map cannot automatically remove stale entries. You need a background process that iterates over keys and cleans them up. If you don't do this, memory leaks. In Rust, for such a task you would use DashMap with manual lifetime management or the moka crate with built-in TTL — but there you'd have to deal with ownership and lifetimes.
Step 3. Profiling. Go has built-in pprof: just import net/http/pprof and take a profile at /debug/pprof/. You can see how much time is spent on garbage collection and which functions are heating up the CPU. In Rust, the analogues are perf and flamegraph, but they require external tools.
What goes wrong if you skip step 1? The application will eat memory and crash with OOM. If you skip step 2 — the same, but slower. If you don't profile — you'll be guessing why latency is growing.
Where the scheme itself breaks
Even the right language choice doesn't guarantee success. Here are four scenarios where the model itself fails.
1. Load conditions changed
A Go service with goroutines per request worked at 1,000 RPS. After a marketing campaign, the load grew to 20,000 RPS. The Go scheduler can't keep up with the goroutine queue, and latencies grow avalanche-like. How to notice in advance: the go_goroutines and go_sched_latencies_seconds metrics. What to do: introduce concurrency limits and backpressure (e.g., via golang.org/x/sync/semaphore).
2. The rule is not applied
In Rust, the borrow checker forbids modifying data while a reference to it exists. In multithreaded code, this results in the need for Arc<Mutex<T>>. If you try to avoid locks with unsafe, you can get a data race that the compiler won't catch. How to notice: stress tests with -race in Go or loom in Rust. What to do: don't overuse unsafe without review.
3. The tool stopped working
The pprof profiler shows a goroutine leak but doesn't say where exactly they are created. You have to manually search for go func(). In Rust, flamegraph may not show inline functions. What to do: add context to logs and use tracing (OpenTelemetry).
4. The deadline passed
You chose Rust for a new microservice, but the team doesn't know the language. Time to production stretches threefold. How to notice in advance: estimate the time to learn ownership and borrowing — usually 2–3 months to confident code. What to do: start with Go for business logic, and introduce Rust selectively for performance-critical modules.
Learning paths and their cost
Self-study through documentation and articles is free but fragmented. Mistakes in Go concurrency or Rust ownership are costly: bugs show up under load. Courses provide structure and feedback but cost money. Open-source practice is the best training ground but requires time to find tasks.
In the Go and Rust — Systems Programming course, they cover exactly such scenarios: limiting concurrency, profiling, memory management. Lessons are personalized to the student's level, and detailed answers can be discussed in an AI chat.
If you want to understand more deeply how to ensure service reliability, check out the material Site Reliability Engineering (SRE).
Who it suits and who it doesn't
It suits you if you write backend in Go and want to understand concurrency and profiling, or if you need to learn Rust for safe memory management in utilities and WebAssembly. It doesn't suit you if you expect quick results without studying theory: Rust takes time to learn ownership, and Go takes time to understand the scheduler.
The Go and Rust — Systems Programming course provides a structured path and analysis of real tasks that are hard to find in documentation. You can start on the course page.
You can start here: Go & Rust — Systems Programming.