Go is fast. Your slow API isn’t a language problem.

Someone on your team just ran a benchmark. Go handles 45,000 requests per second. Your Laravel or FastAPI service tops out at 4,000. There’s a proposal in the group chat. “We should consider moving to Go.”
This story plays out in engineering teams everywhere. I get it. The numbers are real. Go is genuinely fast. But I’ve seen three “rewrite in Go” projects up close, and two of them solved a problem that wasn’t actually the problem.
Before you spec out the migration, let me walk you through what the benchmarks don’t show.
The Benchmark Is Not Lying. It’s Just Not Your Reality.
Go’s performance advantage is genuine and well-documented. Go v1.24, introduced a new built-in map implementation based on Swiss Tables and runtime optimizations that reduce average CPU overhead by 2–3% across standard benchmarks. GC pause times improved by 15–25%. Goroutines start with a 2KB stack versus the roughly 1MB a typical OS thread reserves, which means a Go service can sustain hundreds of thousands of concurrent connections without collapsing.
But there’s a catch. Most benchmarks that show Go running 10–30x faster than PHP or Python are testing CPU-bound work: raw computation, serialization loops, in-memory data processing. These are conditions designed to show what a language runtime can do in isolation.
Your API is not doing that. Not mostly.
A typical production web service spends the majority of each request’s time waiting. Waiting on a database query to return. Waiting for a Redis GET. Waiting for an external HTTP call to respond. This is I/O-bound work, and when you account for it, the performance gap between Go and a well-tuned PHP or Python service shrinks dramatically. A Gin endpoint and a FastAPI endpoint hitting the same Postgres query will finish within milliseconds of each other when both have proper connection pooling, indexed tables and a warm cache. The language runtime is not the bottleneck. It rarely is.
What’s Actually Slowing Your Service Down
I’ve profiled enough production APIs to have a consistent shortlist of actual culprits.
Unindexed queries. A query that takes 800ms drops to 12ms with a composite index. No rewrite required. This is embarrassing every time, and it happens more often than people admit.
N+1 queries hidden inside ORMs. You load a list of orders, then loop and fetch each user in a separate query. Your ORM made it easy enough to write that nobody caught it in review. A single JOIN or eager load fixes this at the application layer, in the same language you’re already in.
No caching. If the same expensive query runs on every request, that’s an architecture problem. Redis with a sensible TTL solves it. The language your service is written in has nothing to do with it.
Synchronous operations that belong in a background job. Sending email inside the request lifecycle. Generating a PDF and waiting for it. These belong in a queue. Push the work off the critical path, return the response immediately and process it in the background. Your users don’t care whether you wrote the worker in Go or PHP.
Missing connection pooling. Opening a new database connection per request is slow, and it’s the kind of thing that looks fine in development and collapses under load. Most frameworks handle this by default, but it’s worth verifying your config reflects reality.
Fix any two of these in your existing codebase and you will see more improvement than a language migration delivers.
The Real Cost of “Let’s Rewrite in Go”
Even if Go is the right call long-term, the cost of getting there is consistently underestimated.
Your team knows your current stack. They know the edge cases. They know where the bodies are buried. A developer who has worked with Laravel for three years has intuitions that took three years to build. That knowledge does not migrate to Go. You start from scratch.
The library ecosystem shifts under you. PHP’s Composer and Python’s pip ecosystems have mature libraries for nearly every problem a business API encounters: payment processing, PDF generation, OAuth flows, CSV parsing, SMS, image manipulation. Go has equivalents for most of these, but “has an equivalent” doesn’t mean “as mature” or “as well-documented.” You’ll spend time solving problems your current stack already solved.
Debugging and observability reset. You know how to read your current stack traces. You know which log line maps to which request. In Go, you rebuild all of that familiarity from zero. This isn’t permanent, but the cost during the transition is real and it lands right when you can least afford it.
Hiring. Go developers are fewer and generally more expensive than PHP or Python developers. For a solo tech lead or a small team, this affects your actual budget, not just a spreadsheet exercise.
None of this means “never use Go.” It means the decision should be made with a clear view of what you’re trading away, not just what you’re gaining.
The Narrow Window Where Go Is the Right Answer
Go genuinely shines in specific conditions, and if your workload fits these, the case for it is strong.
High-concurrency, long-lived connections. If you’re building WebSockets at scale, a real-time notification service or a system that needs to hold tens of thousands of open connections simultaneously, goroutines are a structural advantage. The Go runtime multiplexes many goroutines onto a smaller number of OS threads, so you’re not paying OS-thread overhead per connection. That matters at this scale in ways that no amount of PHP or Python tuning can fully offset.
CPU-bound background work. Image processing, video transcoding, compression pipelines and ML inference at the edge. These are places where Go’s compiled runtime earns its keep. A Python script doing the same CPU-intensive work will take meaningfully longer.
CLI tools and single-purpose sidecar services. A Go binary with no runtime dependencies is a pleasure to deploy. If you’re writing an internal tool, a deployment helper or a small service that does exactly one thing, Go’s compile-to-binary model is a real win with no meaningful downside.
Network proxies and protocol-level work. Anything sitting between clients and services, handling fan-out, aggregation or protocol translation, fits Go’s concurrency primitives naturally. Channels and goroutines are a better mental model for this than spawning threads.
If your service doesn’t fit at least one of these categories, the performance case is weaker than the benchmark suggested.
Before the Rewrite: A Practical Checkpoint
If there’s a confirmed performance problem and the team is considering a language migration, do this first.
Profile your slowest endpoints under actual production traffic patterns, not synthetic load tests. Use query logging, check the EXPLAIN output from your database and look at your cache hit rate. In nearly every case, you find the problem before you find a language ceiling.
Here’s a pattern I reach for in FastAPI services. This is async fan-out, pulling from two services in parallel without blocking the event loop:
import asyncio
import httpx
async def fetch_user_and_orders(user_id: int) -> dict:
async with httpx.AsyncClient() as client:
user_resp, orders_resp = await asyncio.gather(
client.get(f"http://internal/users/{user_id}"),
client.get(f"http://internal/orders?user_id={user_id}"),
)
return {
"user": user_resp.json(),
"orders": orders_resp.json(),
}And the equivalent in Go using goroutines:
package main
import (
"encoding/json"
"fmt"
"net/http"
"sync"
)
type FanOutResult struct {
User map[string]interface{}
Orders []interface{}
}
func fetchUserAndOrders(userID int) (FanOutResult, error) {
var wg sync.WaitGroup
var result FanOutResult
var mu sync.Mutex
var fetchErr error
fetch := func(url string, dest interface{}) {
defer wg.Done()
resp, err := http.Get(url)
if err != nil {
mu.Lock()
fetchErr = err
mu.Unlock()
return
}
defer resp.Body.Close()
mu.Lock()
json.NewDecoder(resp.Body).Decode(dest)
mu.Unlock()
}
wg.Add(2)
go fetch(fmt.Sprintf("http://internal/users/%d", userID), &result.User)
go fetch(fmt.Sprintf("http://internal/orders?user_id=%d", userID), &result.Orders)
wg.Wait()
return result, fetchErr
}Both solve the same fan-out problem. The Go version is more verbose and requires you to manage synchronization manually. The Python version reads close to sequential code. When concurrency is the bottleneck, Python’s asyncio with httpx gets you most of Go's concurrency benefit on I/O-bound paths, without the migration cost.
What you should notice here is not that one is better. It’s that both can solve the problem. The question is which one gives your team the fastest time-to-correct in production when something breaks at 2 AM.
How to Actually Make This Call
Here’s the framework I’d use as a solo tech lead or a small-team engineering manager.
First, profile under production conditions. If your 95th-percentile response time is under 200ms and your servers have room, you don’t have a performance problem. You may have an anxiety problem.
Second, compare the cost of an optimization against the cost of a rewrite. Fixing an N+1 query takes an afternoon. A service rewrite with test coverage, updated CI pipelines and team ramp-up takes months. The ROI math almost never favors the rewrite unless the service is genuinely hitting a language ceiling.
Third, scope the migration carefully. If Go is the right tool for a specific new service that handles high-concurrency streaming, that’s a reasonable place to start. Rewriting your entire business logic layer because a benchmark was impressive is not.
Fourth, be honest about motivation. Sometimes “we should use Go” is a hiring pitch or a resume line. That’s not nothing, but it should be transparent about what it is, not dressed up as a performance argument.
What This Actually Means for You
Go is an excellent language. The tooling is clean, the standard library is thoughtful and the runtime is genuinely well-designed for concurrency. The improvements to the map implementation and garbage collector made an already-fast runtime faster. When Go is the right tool, it’s a good one.
But the majority of “we need Go” proposals are architecture-work proposals with a different label. The database query doesn’t run faster because you wrote the service in Go. The cache still needs to be warm. The connection pool still needs to be configured correctly. The async patterns still need to be in place.
Ship the architectural fixes first. Profile exhaustively. Make your I/O async, index your queries, cache your hot paths and offload synchronous work to background jobs. If you do all of that and you still hit a ceiling that you can trace to the language runtime itself, then you’ve earned the right to have the Go conversation.
Until that point, the bottleneck is not where you think it is.


