Stop rewriting your Python microservices in Go just for the speed

5 min read

A UK developer's desk with two monitors showing Python profiling flamegraphs and a Go gopher sticker

Every six months I see the same Slack post on r/python or in a Monzo-style engineering chat: "We're rewriting the payments reconciler in Go because Python is too slow." Then six months later, nobody can remember what the rewrite actually fixed. If you keep feeling pressure to stop rewriting your Python microservices in Go just for the speed, read this first and profile before you port.

The real reason your service is slow isn't the GIL

I have profiled enough services at UK fintechs and public-sector shops to bet money that your p99 latency problem is one of three things: serialised database calls, a chatty HTTP boundary, or an unbounded list(...) you can fit in a spreadsheet. The GIL is usually a distant fourth. Go does not magically fix SELECT * across a million-row payments table, and Go does not magically fix N+1 queries against an Oracle instance at HMRC-scale. Those are query-shape problems, and the fix is a query-shape solution: an index, a denormalised read model, a Materialized View, or simply prefetch_related().

When the BBC's iPlayer team talked publicly about their Python stack handling tens of thousands of requests per second on the backend, the point was never "Python is intrinsically fast." It was: they made the right bets in the right layers. A rewrite in Go would have cost them a year and delivered a 10% improvement at best. Spend that year on Postgres tuning and connection pooling first.

Where Go actually wins, and where it doesn't

Go is genuinely brilliant at CPU-bound, allocation-heavy workloads with predictable shapes: protobuf parsing at scale, JSON streaming pipelines, anything that looks like a proxy or a queue consumer. That is where the 5x-10x speedups people quote come from. If your service is doing json.loads() on a 4MB blob in the hot path 50,000 times per second, yes, Go's encoding/json or simdjson-go will trounce CPython's pure-Python parser.

Where Go does not win is the I/O-bound CRUD service. A FastAPI or Flask handler waiting on Postgres, S3, and a third-party API spends maybe 0.3ms of CPU per request. Doubling that with Go gives you 0.15ms back on a 120ms request. Nobody notices. What they do notice is that your new Go service now needs go.mod, a separate Dockerfile base image, a separate CI matrix, a separate on-call rotation, and a separate mental model for goroutine leaks. That is a real cost. Put it on the same spreadsheet as the speedup, in column B.

Profile first: a concrete checklist

Before anyone opens a Go repo, run the actual profiling tools. Pyroscope, OpenTelemetry tracing, and cProfile with snakeviz cost you an afternoon. The signal is almost always the same.

  • Trace one slow request end-to-end. Find the longest span that is not your code. That is your bottleneck.
  • Check for sequential awaits that could be asyncio.gather(). I covered this in the asyncio-vs-threading piece, but the short version: gather the independent calls.
  • Run EXPLAIN ANALYZE on your top five queries. If you see Seq Scan on a table over 10k rows, fix that before touching language choice.
  • Look at your serialiser. orjson is roughly 2-4x faster than the stdlib on Python 3.12+. msgspec is faster again.

A quick win you can ship today

If your service is bottlenecked on JSON, this is a 5-line change. Drop in orjson, route FastAPI's default response through it, and rerun your load test.

from fastapi import FastAPI
from fastapi.responses import ORJSONResponse

app = FastAPI(default_response_class=ORJSONResponse)

@app.get("/payments/{pid}")
async def get_payment(pid: str):
    row = await db.fetch_one(
        "SELECT id, amount, currency, settled_at FROM payments WHERE id = $1",
        pid,
    )
    return {"id": row["id"], "amount": row["amount"], "currency": row["currency"]}

That single line, default_response_class=ORJSONResponse, routinely gives 30-50% throughput back on JSON-heavy endpoints with zero rewrite. If your boss wanted Go for that win, you just saved a quarter.

The honest TCO of a Python-to-Go rewrite

Let's price it. A realistic UK microservice rewrite is 3-6 engineer-months if the domain logic is simple, 12-18 if it isn't. Then you maintain two languages, two dependency pipelines, and two on-call pools. At a typical London fintech day rate, you are looking at £80k-£250k of pure engineering cost, before the productivity tax of context-switching.

The benchmark to beat is brutal. From the TechEmpower JSON roundtrips and the published Cloudflare work, a tuned CPython 3.12 service with uvicorn and orjson sits comfortably between 40k-80k requests per second on a single core for trivial responses. Most "rewrite for speed" services are nowhere near that ceiling; they are doing 200 requests per second and have a N+1 problem. The GDS and NHS Digital teams running Python in production at scale would tell you the same thing: profile, then decide.

When Go is the right call

Be honest about this list. Rewrite in Go when:

  • You have profiled and the CPU time is genuinely in your own code, not I/O.
  • You need true parallelism on a single box for a CPU-bound job and multiprocessing is a worse fit than goroutines.
  • You are shipping a binary to customers who cannot rely on a Python runtime (Monzo's tooling, embedded agents, edge functions).
  • You are building new infrastructure, not replacing a working service.

If your justification is "Go is faster," you have not yet earned the rewrite. Get the trace, get the flamegraph, get the query plans, and come back with numbers. The fastest Python service is the one you did not rewrite, and the most expensive Go service is the one that replaced a 200 rps CRUD handler with a 250 rps one and a six-figure bill.

FAQ

Is Python really slow compared to Go?

Yes, for CPU-bound pure-Python work, Go is typically 5x-50x faster. For typical I/O-bound web services, the gap is usually under 2x and is dominated by database and network latency, not language speed.

Should I use asyncio to make my Python service faster?

Only if your bottleneck is waiting on many independent I/O calls at once. Asyncio does not speed up CPU-bound code and adds real complexity. Profile first.

What is the fastest way to speed up a Python API without rewriting it?

Swap the JSON serialiser for orjson or msgspec, add a Postgres index on your hot query path, enable HTTP keep-alive and connection pooling, and run under uvicorn with multiple workers. Most teams stop here.