Go, Rust, or Python: picking a backend at scale
When success at scale strains Python, the fix is rarely a rewrite. How to choose Go, Rust, or Zig for the one service that needs it.
Somewhere between your Series B and your Series C, an engineer will say it in a planning meeting: “We’re big enough now. It’s time to move off Python.” The sentence lands like a graduation. Real companies at real scale write Go. Or Rust. Maybe Zig. Python was fine for getting here, and now we have arrived.
It is the wrong frame, and it is an expensive way to be wrong.
Scale does not retire Python. It tells you, with a profiler, exactly which one of your services has a cost Python cannot pay. That is a much smaller and much more useful fact than “we have outgrown the language.” The companies people cite as proof that you leave Python at scale did not leave Python. They found the one component that was bleeding and specialized it. The rest still runs Django.

Scale doesn’t kill Python. It exposes two specific costs.
Instagram runs one of the largest Django deployments on earth. They did not rewrite it in Go. They forked CPython, called it Cinder, and pulled 1.5x to 4x out of the runtime with inline caching and a method-at-a-time JIT. The largest Python shop you can name looked at “rewrite in a faster language” and chose “make Python faster” instead. That should tell you how rarely the language itself is the thing standing between you and the next order of magnitude.
Python has two costs that scale makes visible, and naming them precisely matters, because they point at different answers.
The first is the GIL. Standard CPython runs one thread of Python bytecode at a time, so a CPU-bound service cannot use the cores you are paying for. You work around it with processes instead of threads, which costs memory and coordination, or you push the hot work down into C. Free-threaded, no-GIL builds are real now: experimental in Python 3.13 and officially supported in 3.14. The honest posture is to test against them, not ship on them: the single-threaded penalty ran 20 to 40% in the 3.13 build, and much of the C-extension ecosystem is still catching up.
The second is the per-request tax: garbage collection and interpreter overhead you pay on every call, whether or not you have a concurrency problem. This is the cost that shows up as a cloud bill rather than a latency chart. A service that is expensive but not slow is still worth making cheaper, and that is where a compiled language earns its keep, because the gap between scrimping bytes and splurging gigabytes is a line item someone signs.
Neither cost means “leave Python.” Both tell you where to look. And plenty of services at scale are I/O bound, sitting idle while they wait on a database or another API, where the runtime is not the ceiling at all and a rewrite would buy you nothing but a second language to keep alive.
What a lower-level language buys, and what it taxes
When the profile does point at the runtime, what you are buying is one of three concrete things: predictable latency with no garbage-collector pauses, a smaller memory and CPU footprint per request, or a type system strict enough to make whole classes of bug impossible to compile. Those are real, and they convert directly into either a better user experience or a smaller bill.
The tax is just as real, and it is easy to underweight while the benchmark is glowing. A second language is a second toolchain, a second library ecosystem that is usually less mature than Python’s, a second on-call skill set, and a hiring pool that is smaller and more expensive. Zig and C hand you manual memory management, which is control and also a foot-gun. Rust hands you the borrow checker: a real guarantee, and a wall every new hire has to climb. None of that argues for staying on Python. It is the other side of a ledger you have to actually balance instead of admire.
Here is the same decision as a lookup, once you know what your profiler is telling you:
| If the profile points at | Reach for | Because | Proven in |
|---|---|---|---|
| GC pauses or tail-latency spikes in one hot service | Rust | No garbage collector, so no stop-the-world pauses | Discord Read States |
| Illegal states slipping through under concurrency | Rust | The type system makes invalid states impossible to compile | Dropbox Nucleus |
| Huge concurrent connection counts, broad hiring | Go | Cheap goroutines, a small language, one static binary | Uber, Twitch |
| A systems primitive needing C-level control | Zig | No hidden allocations, clean C interop | TigerBeetle |
| One hot loop, the rest fine in Python | Rust extension (PyO3) | Native speed behind an unchanged Python API | pydantic-core, Polars |
| Nothing conclusive: I/O bound or code-bound | Stay on Python | The runtime was never the ceiling | Instagram (Cinder) |
Match the language to the shape of the bottleneck
Once you have profiled and you know which service hurts and why, the choice almost makes itself. The mistake is picking the language by reputation instead of by the shape of the pain.
- Tail latency you cannot smooth out, in one hot service: Rust. Discord’s Read States service sat in the hot path of every connect, every message sent, every message read. Its Go version spiked to 10 to 40 milliseconds of latency, on a clockwork cycle, every couple of minutes. The cause was not sloppy code. They had few allocations and a hand-tuned implementation. It was Go’s garbage collector scanning the entire LRU cache to prove nothing could be freed. You cannot tune your way out of that, because the collector runs anyway. Rust has no GC, so the spikes had nowhere to come from. Rewritten, average latency dropped from milliseconds to microseconds and memory shrank by roughly a factor of eight. In Discord’s own words, “even with just basic optimization, Rust was able to outperform the hyper hand-tuned Go version.”
- Correctness under concurrency, where a wrong state is worse than a slow one: also Rust. Dropbox rebuilt its sync engine, Nucleus, in Rust. Sync is a distributed-systems client: it runs on half a billion devices, against every strange filesystem configuration on earth. The win was not only speed. Rust’s type system let them design invalid states out of existence, so entire categories of sync bug could not compile in the first place. When the failure mode is silent data corruption, “it will not compile” is worth more than “it is fast.”
- Heavy concurrent network I/O, and you want to hire broadly: Go. This is the common case, and Go is usually the right answer to it. Uber and Twitch, the latter moving off Python, lean on cheap goroutines to hold enormous numbers of simultaneous connections. Go’s surface area is small enough that engineers you already have can pick it up, the talent pool is deep, and a service ships as a single static binary with nothing to install around it. Go has a garbage collector too, and for almost everyone it is a non-issue: its collector holds pauses well under a millisecond. Discord’s trouble was a pathological interaction with one giant, long-lived cache, not a reason for the rest of us to fear Go.
- A systems primitive where you want C-level control and no surprises: Zig, with eyes open. Zig’s pitch is no hidden allocations and no hidden control flow, with clean C interop. TigerBeetle, a financial database, chose it for exactly that: static allocation up front, total control over memory, safety rules borrowed from aerospace. That is a defensible choice for infrastructure you are building down at the metal. But notice how narrow it is, and notice the counter-signal: Bun, the highest-profile Zig project in the world, has announced it is moving to Rust, partly for stronger C++ interop and a deeper hiring pool. Zig is a scalpel for people building databases and runtimes. It is almost never the answer to “our checkout service is slow.”
The cheapest specialization keeps Python
There is an option the “rewrite it in Rust” argument usually skips. Often you do not move the service at all. You move the hot loop, and leave everything around it in Python.
This is the pattern behind two libraries you may already run in production. Pydantic, the validation layer under a large share of Python web services, rewrote its core in Rust for version 2 and multiplied its throughput while the API stayed pure Python. Polars, the dataframe engine people reach for when pandas runs out of headroom, is Rust underneath a Python skin. In both cases the Python developer notices nothing except that the code is faster.
You can do the same inside your own codebase with PyO3, which lets you write a Rust extension and import it from Python like any other module. Profile, find the small fraction of the code burning most of the CPU, and rewrite that fraction. The service stays in Python, your team stays fluent in Python, and the hot path runs at native speed. It is the most surgical version of the whole argument: specialize the bottleneck, not the codebase. Before you plan a migration, ask whether a single extension module gets you most of the win at a fraction of the exposure.
The rewrite isn’t the cost. The org is.
The architecture diagram hides the real cost. A full rewrite in Rust does not cost you a rewrite. It costs you a hiring market. Rust salaries run high, a US average around $145,000 with senior roles reaching past $200,000, the experienced talent pool is thin, and the borrow checker has a genuine learning curve even for strong engineers. You are not only buying a faster service. You are buying a permanent change to who you can hire and how readily they contribute.
That is fine when the service justifies it. It is a disaster when it does not, and the failure mode is specific: an engineer ships a Rust rewrite that is ten times faster on a service whose cost was a rounding error on the cloud bill, and whose latency was never losing a customer. Technically flawless. Business-irrelevant. That is not a win you can put in front of a board.
So the discipline is boring, and it is the whole game: profile first. The improvement you want might be a database index, a caching layer, a garbage-collector setting, or one N+1 query fixed in the code you already have, for far less risk than a migration and a new hiring problem. Reach for a lower-level language when you have proven, with numbers, that the ceiling is the language and not your own code.
The senior move is knowing when not to
Look again at the companies held up as proof that you leave Python at scale. Discord rewrote one service, not Discord. Dropbox rewrote the sync engine, not its web backend, which is still Python. Instagram rewrote nothing to another language, it made its Python faster. The real pattern is not migration. It is selective specialization: isolate the single component whose cost has turned against you, and rewrite only that, in the language that matches the specific way it hurts.
That call takes judgment no benchmark hands you: whether the bottleneck is the language or the code, whether the fix solves a business problem or just a fun one, whether a second language is worth the permanent tax on your team. It is the kind of decision we work through with clients inside the Tarmac 10, staffed by senior engineers who have made the wrong version of this call before and carry the scar. When a service genuinely needs to drop to Rust or Go, our engineering teams do it as a scalpel, not a migration.
Python got you to scale. At scale, the achievement was never the new language. It was the profiler, and the judgment to use it before writing a line of anything else.