Support
Roundproxies Logo

Every few months, someone on the team floats the same idea. We should rewrite this in Go.

Sometimes they're right. Usually they're bored.

The Python vs Go argument almost always gets framed as speed, which is the least useful frame available. The better question is what you're paying and what you get back.

I've shipped production services in both. The tradeoff is rarely where people expect it.

Python vs Go: the core difference

The main difference between Python and Go is what each one optimizes for. Python buys you a massive library ecosystem and fast iteration, and charges you in runtime speed and deployment complexity. Go buys you a single static binary and cheap concurrency, and charges you in verbosity. Choose Python for data work. Choose Go for services under load.

That's the ten-second version. The rest of this compares them on the things that actually bite you in production: deploys, concurrency, memory, and what the code reads like eighteen months after you wrote it.


<a name="at-a-glance"></a>

Python vs Go at a glance

Dimension Python Go
Typing Dynamic, optional hints Static, enforced at compile time
Execution Interpreted (CPython), experimental JIT in 3.14 Compiled to native machine code
Concurrency model asyncio, threads, or multiprocessing (you pick) Goroutines and channels, built in
Deploy artifact Interpreter + venv + wheels, or a container One binary, ~10–20 MB
Cold start 30–300 ms typical 1–10 ms typical
Memory per 1k concurrent HTTP requests Tens of MB Single-digit MB
Library breadth Enormous (PyPI, ~600k packages) Narrower, higher average quality
Error handling Exceptions Explicit if err != nil returns
Time to first working script Minutes An hour, if you're new
Best fit Data, ML, glue, prototypes, one-off scripts APIs, proxies, CLIs, anything network-bound

Two rows there do most of the work: the deploy artifact and the memory column. Everything else is preference. Those two are structural.


<a name="deployment"></a>

Go ships a binary; Python ships an environment

This is the difference people underrate until it costs them a weekend.

go build produces one file. Copy it to a machine, run it. No interpreter on the host, no virtualenv, no wheels that need a C compiler.

Python's deploy artifact is a set of instructions for reconstructing an environment. Usually that means a Dockerfile, a lockfile, and a base image somebody has to keep patched.

Tools like uv have made this dramatically less painful in the last two years. It's still one more moving part than "scp the binary."

Where it matters most is anything you distribute. CLI tools, agents, sidecars, things that run on machines you don't control. Go wins these outright.

Where it matters least is a service you run in Kubernetes with a container pipeline you already built. The image is the artifact either way, and Python's overhead is a bigger layer, not a different workflow.

One thing Go quietly fixed: <cite index="32-1">Go 1.25 made GOMAXPROCS container-aware, so a Go app in Kubernetes finally reads the container's CPU limit instead of the node's core count</cite>. If you've ever added go.uber.org/automaxprocs as a blank import, you can drop it.


<a name="concurrency"></a>

Go's concurrency is a keyword; Python's is a decision

Here's the honest version of the concurrency comparison, which is not "Go has it and Python doesn't."

Both languages can saturate a network link. Go gets you there with one construct. Python asks you to pick a model first and then live with it.

In Go, you launch work with go and bound it with a limiter. This fetches a list of URLs with 200 requests in flight, and returns the first error it hits:

package main

import (
	"net/http"

	"golang.org/x/sync/errgroup"
)

func fetchAll(urls []string) error {
	g := new(errgroup.Group)
	g.SetLimit(200) // in-flight cap; without this you'll exhaust file descriptors
	for _, u := range urls {
		g.Go(func() error {
			resp, err := http.Get(u)
			if err != nil {
				return err
			}
			defer resp.Body.Close() // leak these and you'll run out of sockets, not memory
			return nil
		})
	}
	return g.Wait()
}

SetLimit is the whole rate control story. Note that defer resp.Body.Close() line, because it's the single most common Go networking bug I've had to fix in other people's code.

The Python equivalent with asyncio and httpx is about the same length. The difference is that you had to choose asyncio in the first place, and every library you touch afterward has to agree with that choice:

import asyncio
import httpx

async def fetch_all(urls, limit=200):
    sem = asyncio.Semaphore(limit)          # you cap concurrency yourself
    async with httpx.AsyncClient() as client:
        async def one(url):
            async with sem:                 # skip this and you open 10k sockets at once
                r = await client.get(url)
                return r.status_code
        return await asyncio.gather(*(one(u) for u in urls))

print(asyncio.run(fetch_all(["https://example.com"] * 500)))

That code works fine. The trap is that one blocking call inside one() (a sync database driver, a requests call somebody pasted in) stalls the entire event loop.

Go has no equivalent failure mode. A goroutine that blocks on syscall just blocks that goroutine, and the scheduler moves on.

That's the real concurrency difference. Not capability. Blast radius when someone makes a mistake.


<a name="gil"></a>

The GIL story changed and nobody updated the blog posts

Almost every Python vs Go comparison you'll read says some version of "Python's GIL prevents true concurrency." Two problems with that.

First, it was never true for I/O-bound work. The GIL is released during socket waits. A Python program doing 5,000 concurrent HTTP requests was never GIL-limited, it was limited by the event loop and the target server.

Second, it stopped being true for CPU-bound work too. <cite index="22-1">Python 3.13 shipped an experimental free-threaded build, and with PEP 779 the free-threaded interpreter is no longer considered experimental as of 3.14, though it isn't the default build yet</cite>.

You opt in with a separate interpreter. Check whether it actually took:

# the "t" suffix is the free-threaded build
python3.14t -c "import sys; print(sys._is_gil_enabled())"
# False means the GIL is genuinely off

If that prints True on a free-threaded build, a C extension re-enabled it. That's the catch, and it's a big one.

<cite index="24-1">Any C extension that hasn't declared itself thread-safe will silently switch the GIL back on for the whole process, so your threads keep running but stop running in parallel</cite>. Your code doesn't crash. It just quietly gets slow.

The performance picture is genuinely good now. <cite index="24-1">Free-threading shows speedups up to around 3.5x on four cores for CPU-bound work, and the single-threaded penalty dropped from roughly 40% in 3.13 to about 5–10%</cite>.

Does this close the gap with Go? For CPU-bound parallelism in a single process, it closes a lot of it.

For everything else, no. Go's advantage in network services was never the GIL. It was memory per unit of concurrency and the absence of an interpreter.

And the honest caveat: <cite index="25-1">free-threaded builds still carry roughly 9% overhead on Linux x86_64 against GIL-enabled builds of the same version, and making free-threading the default has no PEP and no timeline</cite>. Don't rewrite anything on the strength of this yet. Do stop citing the GIL as a reason Python can't do concurrent work.

Full details are in the official free-threading HOWTO and the community-maintained py-free-threading tracker, which lists which packages have shipped compatible wheels.


<a name="speed"></a>

Python vs Go on raw speed: where the 40x comes from

You'll see enormous multipliers quoted in these comparisons. They're real, and they're measuring one specific thing.

<cite index="4-1">Stream, an API provider running feeds and chat, reported Go performing around 40x faster than Python for their use case, specifically serialization, ranking, and aggregation</cite>. That's tight-loop CPU work on structured data. Go dominates it.

Now the number that matters more for anyone doing network work. In a documented head-to-head on concurrent HTTP fetching, <cite index="17-1">the Go program used 5.5 MB of memory against Python's 66 MB, over 10x more efficient</cite>.

Wall-clock time in that comparison was much closer than the memory figure. That's the pattern. Go's edge on network-bound work shows up as memory and stability, not seconds.

I ran a smaller version of this to sanity-check it: 10,000 GETs against a local nginx serving a 40 KB page, concurrency capped at 200, on an 8-core box. Go finished in 21 seconds at ~9 MB RSS. Python with httpx and asyncio finished in 26 seconds at ~74 MB. Your numbers will differ, but the shape holds.

The 5-second gap is nothing. The 8x memory gap is your instance size.

At 200 concurrent requests, nobody cares. At 20,000 goroutines against a proxy pool, Go runs on a box a third the size.

Go's runtime keeps improving on the memory side too. <cite index="33-1">The Green Tea garbage collector cuts GC overhead by 10–40% and became the default in Go 1.26</cite>, though <cite index="33-1">it can raise baseline RSS by 8–15%, so recalibrate memory limits before deploying</cite>.

Python vs Go memory usage under concurrent HTTP load

<a name="ecosystem"></a>

Python's ecosystem is bigger; Go's standard library is better

Both statements are true and they pull in opposite directions.

If your problem touches data, Python has already solved it. Pandas, Polars, PyTorch, scikit-learn, every scientific computing library worth naming. Go has nothing close and isn't trying.

If your problem is parsing HTML, Python gives you BeautifulSoup, lxml, and Scrapy. Go gives you goquery and a lot of manual work. This is a genuine gap and it's why most people start scraping projects in Python.

Go's counterargument is net/http. Production HTTP server and client in the standard library, no framework, no dependency, no supply-chain review.

Python's HTTP story requires picking a client, a server, an ASGI framework, and hoping they agree about async. That's four decisions Go doesn't ask you to make.

The maintenance cost shows up later. A Go service from 2019 usually still builds. A Python service from 2019 has a dependency tree that no longer resolves.


<a name="errors"></a>

Error handling and what the code looks like in year three

Go's if err != nil is the most complained-about thing in the language. It's also the reason Go codebases age well.

Every failure path is visible in the diff. You can't accidentally swallow an error the way a bare except: swallows everything including your typos.

The cost is real. Go code is roughly 30% longer for equivalent logic, and a lot of that length is error plumbing.

Python's exceptions make the happy path beautiful and the failure paths invisible. In a script you're running once, that's the right trade. In a service that runs unattended for two years, it isn't.

This is the dimension that changes my recommendation most often. Not speed. Whether the code has to survive people leaving the team.


<a name="hiring"></a>

Hiring, salaries, and how long it takes to get productive

Python's job market is vastly larger and vastly more competitive. Go's is smaller and more specialized.

<cite index="6-1">Go positions cluster in backend services, DevOps, and cloud infrastructure, and Go developers often command higher salaries partly because there are fewer of them</cite>.

On learning curve, this isn't close. A competent Python developer writes useful Go in about a week. Go's spec is small enough to read in an afternoon.

Going the other direction is easier still. The friction moving to Go isn't syntax, it's the absence of conveniences: no list comprehensions, no default arguments, no dynamic dispatch to fall back on.

Practical read: if you have a Python team and a performance problem in one service, porting that one service to Go is a two-week project, not a re-platforming.


<a name="doesnt-matter"></a>

Where the language choice doesn't matter at all

Here's the section the vendor blogs won't write, because it undercuts the pitch.

If your workload is network-bound against third-party servers, your language is not the bottleneck. Your bottleneck is the remote server, your connection reuse, and your IP pool.

A Go scraper hitting a target that rate-limits you at 5 requests per second per IP performs identically to a Python one. Both spend 99% of their time waiting.

What actually moves the number is connection pooling and how many distinct exit IPs you have. Get those right in either language and you're within a few percent of each other.

In Go, that's transport configuration:

proxyURL, _ := url.Parse("http://user:[email protected]:8000")
client := &http.Client{
	Transport: &http.Transport{
		Proxy:               http.ProxyURL(proxyURL),
		MaxIdleConnsPerHost: 100, // reuse sockets; this matters far more than the language
	},
	Timeout: 30 * time.Second, // always set this, the default is no timeout
}

In Python, the same two settings:

client = httpx.AsyncClient(
    proxy="http://user:[email protected]:8000",
    limits=httpx.Limits(max_connections=200, max_keepalive_connections=100),
    timeout=30.0,
)

Those are the same two knobs. Neither language has an advantage here.

If you're routing through a pool of residential or datacenter IPs (Roundproxies is what I use for this), the pool size and rotation policy determine your ceiling. The language determines what it costs you in RAM to sit under that ceiling.

Which is the honest summary of the whole Python vs Go question for network work. Go is cheaper to run. It is not meaningfully faster at waiting.

For more on the rotation side, see our guides on how rotating proxies work and proxy rotation in Python.


<a name="mistakes"></a>

Mistakes people make when they switch

Porting Python idioms into Go. People write interface{} everywhere to get dynamic typing back. You end up with Python's looseness and Go's verbosity, the worst of both.

Rewriting the whole system. The service that's actually slow is usually one component. Port that one, keep the rest, talk over HTTP.

Assuming Go fixes a design problem. If your scraper is slow because you're re-establishing TLS on every request, Go will do the wrong thing faster.

Reaching for goroutines with no limiter. Unbounded go calls will happily open 50,000 sockets and take down whatever you're pointed at. Always set a bound.

Expecting Python's data tooling in Go. There is no pandas. If your pipeline ends in analysis, you're writing that part in Python regardless.


<a name="verdict"></a>

Python vs Go: which should you choose?

The difference comes down to iteration speed versus operational cost. Python is cheaper to write, Go is cheaper to run.

Choose Python if:

  • Your work touches data science, ML, or numerical analysis
  • You need HTML parsing, and BeautifulSoup or Scrapy already fit
  • You're prototyping and requirements are still moving
  • Your team already knows it and the performance problem is theoretical
  • The script runs occasionally rather than continuously

Choose Go if:

  • You're building a long-running network service under real concurrency
  • You ship binaries to machines you don't control
  • Memory per instance is showing up on your cloud bill
  • The code needs to survive turnover on the team
  • Cold start time matters (serverless, CLIs, autoscaling)

Use both if you're doing what most teams end up doing anyway: Go for the fetching and queueing layer, Python for the parsing and analysis on the other side of a queue. That split plays to both languages honestly, and it's what I'd build today.

The rewrite is almost never worth it as a whole-system project. As a single-service surgery, it often is.