The benchmarks are real. The ROI usually isn’t.

There is a particular kind of engineering meeting that happens at companies of a certain size. The team is discussing a latency problem. Someone opens a tab, finds a benchmark showing Go handles 3x more requests per second than Python, and within fifteen minutes the conversation has shifted from “we have a bottleneck” to “we need to rewrite this service in Go.”
This is how it starts. And this is where most teams make their first mistake.
The rewrite conversation is not wrong because Go is bad. Go is genuinely fast. The benchmarks are real. The mistake is treating a language benchmark as a business decision without asking a single question about what kind of work your service actually does.
The Number Everyone Cites (and What It Actually Measures)
In a well-controlled benchmark from mid-2024, a Go-backed REST API on a single core handled 1,194 requests per second against a SQLite database. The equivalent Django setup hit 447 RPS. That is a 2.67x throughput improvement. Scale to four cores: Go reached 3,416 RPS versus 1,308 for Django. The performance gap held.
Those numbers are real. They are also measuring CPU-bound computation with concurrent connections against a local database. Your service almost certainly does not look like this.
Most Python microservices spend the overwhelming majority of their wall-clock time waiting. Waiting for a database query to come back over the network. Waiting for a third-party API to respond. Waiting for a Redis cache hit. When your service is sitting in I/O, it is not consuming CPU. The Python GIL is not blocking anything. Your goroutines would also be waiting.
This is the number the benchmark slides never show you: what percentage of your service’s request latency is Python’s fault?
In the vast majority of production microservices, the answer is somewhere between “very little” and “not at all.”
What FastAPI + Async Actually Gets You
FastAPI with Uvicorn has made async Python genuinely competitive for I/O-bound services. The async model is straightforward: when your route handler awaits an I/O operation, the event loop moves to the next queued request. You are not sitting idle. You are handling something else.
# This is not slow Python. It is idle Python.
@app.get("/user/{user_id}")
async def get_user(user_id: int, db: AsyncSession = Depends(get_db)):
result = await db.execute(
select(User).where(User.id == user_id)
)
return result.scalar_one_or_none()The practical guidance from FastAPI’s own deployment docs is to run 1–2 Uvicorn workers per CPU core for I/O-heavy services. On a 4-core VM, that is 8 workers, each running an async event loop. None of them are waiting on each other during I/O.
For CPU-bound work (image processing, heavy computation, data transformations in hot paths), the GIL is a real constraint. But if you have a CPU-bound microservice written in Python, that is a different class of problem with better-targeted solutions than a full rewrite.
The GIL Is On Its Way Out Anyway
Here is the part of the story that most rewrite advocates either do not know or ignore: the Python community has already acknowledged the GIL problem and is actively solving it.
PEP 703, accepted in 2023, added a free-threaded build to CPython. Python 3.13 shipped with it as an opt-in experimental feature. In Python 3.14, free-threaded Python is no longer experimental, with the single-threaded overhead penalty down from roughly 40% in 3.13 to under 10% on most platforms and compilers.
The timeline for the GIL becoming disabled by default is around 2028–2030, but the direction is clear. Python’s threading model is changing at the language level. If your Go rewrite argument rests entirely on GIL contention, you are building a long-term migration plan around a short-term problem.
Rewriting a service for a performance characteristic that the ecosystem is actively eliminating is a bold choice. Bold in the way that “we migrated off Mongo to Postgres in 2012 and are now migrating back” is bold.
Go’s Actual Advantages (the Ones Worth Taking Seriously)
To be fair to the Go advocates: there are legitimate cases where Go wins by more than a little.
Startup time and binary footprint. A Go binary compiles to a single static executable. A minimal Go Docker image runs under 10 MB. A Python image starting from slim-buster with a typical production dependency set is in the 150-200 MB range. In environments where you are spinning up hundreds of short-lived containers or running at the edge, this matters.
Goroutines under genuine concurrency load. At very high request rates with CPU contention, goroutines outperform Python’s thread model in ways that async I/O does not fully compensate for. A 2026 benchmark comparing LLM gateway implementations found Go-based solutions maintaining sub-50ms p95 latency at 10,000 RPS while Python-based equivalents exceeded that threshold well before reaching similar traffic levels. If you are routing tens of thousands of requests per second through a single service, Go’s concurrency model earns its praise.
Resource efficiency in tight environments. Counterintuitively, Python’s heap memory usage in production is often comparable to Go’s for typical web service workloads. But for services that need to run on minimal hardware (edge nodes, embedded systems, cheap VPS instances), Go’s runtime overhead is predictably lower and easier to reason about.
None of these benefits are irrelevant. All of them require you to confirm your service actually experiences the bottleneck before you spend three months on a rewrite.
The Hidden Costs Nobody Puts in the Slide Deck
The benchmark slides show throughput. They do not show the following:
Time to rewrite correctly. A 2,000-line Python service with tests is not two weeks of Go work for a team that primarily writes Python. Error handling in Go is explicit and verbose by design. You will write more code. You will encounter unfamiliar patterns. Budget accordingly.
Ecosystem gaps. Python’s library ecosystem for data processing, ML inference, HTTP client behavior and async task queuing is years ahead of Go’s. If your service calls a Python SDK for an internal ML model, or uses a library with no idiomatic Go equivalent, you are not just rewriting your service. You are also rewriting or replacing your dependencies.
Hiring and team knowledge. Most backend teams have more Python engineers than Go engineers. Rewriting a critical service in a language only two people on the team know confidently is an operational risk with a slow payoff.
Debugging in production. Python’s introspection tools, profilers and stack traces are mature. Go’s tooling is good but different. The first time you debug a goroutine leak in production without your usual toolchain, you will understand the cost of switching.
A Decision Framework That Actually Holds
Before committing to a Go rewrite, answer these questions honestly:
1. Have you profiled where the time actually goes?
# For a FastAPI service, start here before writing a single line of Go
python -m cProfile -o output.prof your_service.py
# Or in production, use py-spy for a zero-overhead sampling profiler
py-spy top --pid <service_pid>If the hottest functions are your own application code doing computation, that is a real argument for Go. If they are asyncio.sleep, await redis.get or await db.execute, the bottleneck is not Python.
2. Is the current service actually hitting a ceiling?
A service that handles 500 RPS and has headroom to 4x that before infrastructure costs bite is not a performance emergency. A 3x throughput improvement on a service you never need to scale past its current load is a 3x improvement in a number that does not matter.
3. Can you hit the target with less disruptive changes?
Horizontal scaling, connection pooling tuning, query optimization and caching layers are boring compared to rewriting in Go. They are also reversible, faster to implement and lower risk. Exhaust these options first.
4. Is this genuinely a new service or a greenfield component?
If you are building something new and the team has Go experience, using Go is a reasonable default choice for high-throughput services. The calculation is different when you are talking about replacing a working system that is not broken in ways a language change would fix.
When the Rewrite Is Actually Correct
There are real scenarios where moving a Python service to Go is the right call:
- You have profiled a CPU-bound hot path that contributes meaningfully to p95 latency and the fix requires true parallelism, not better async usage.
- The service handles very high concurrency (5,000+ concurrent connections) and you have measured that Python’s event loop becomes the bottleneck under sustained load.
- You are building infrastructure tooling (a proxy, a gateway, a sidecar) where binary size, startup time and memory footprint directly affect operational costs.
- Your team has significant Go expertise and the service is being built from scratch, not migrated from a working Python implementation.
Short of these conditions, “Go is faster in benchmarks” is not a sufficient argument. It is a fact about a different program under different conditions than yours.
The Unsexy Answer
The most common fix for a Python service with a latency problem is not a rewrite. It is adding a database index that someone forgot. Or moving a synchronous blocking call into a background task. Or noticing that you are making twelve sequential database queries in a route that could be two. Or discovering that the performance issue only happens at a specific traffic pattern that careful async structuring would handle.
These are unglamorous. They do not require a new repository, a migration plan or three months of team bandwidth. They do not generate a conference talk.
But they ship in a week and they fix the problem.
Go is a good language. It deserves to be used where it earns its adoption: in new high-throughput services where the team knows it well, in infrastructure components where binary size genuinely matters and in services where profiling has conclusively shown that Python’s runtime is the constraint.
Using it because a benchmark looked impressive in a meeting is how you end up with a slower team, a split codebase and the same latency problem you had before, now written in two languages.
Profile first. Rewrite only what the data demands. Everything else is expensive storytelling.


