Is Go Actually Faster? Benchmarking Six Languages for Latency and Throughput.

Low-level is fast. Interpreted is slow. Async is faster. The database is the bottleneck. These assumptions feel like engineering wisdom. Most of them are wrong, or at least far more complicated than they sound.

Some assumptions get repeated so often they stop feeling like opinions and start feeling like facts. I wanted to put some to the test regarding Backend Performance (Latency + Throughput).

🏎️ Latency#

Assumption: “Native code is faster than Interpreted code”, But is it faster in the things that matter?

My test case for latency is to benchmark 6 languages with a simple REST API, for more details about the implementation have a look at REST API and in memory implementation.

LanguageCompiled ToWhen Compilation HappensExecution Model
TypeScriptBytecodeAt runtimeInterpreter + JIT
PythonBytecodeAt runtimeInterpreter
JavaBytecodeBuild timeInterpreter + JIT
C#BytecodeBuild timeInterpreter + JIT
GoMachine codeBuild timeDirect execution
KotlinBytecodeBuild timeInterpreter + JIT

I would say the general assumption of the podium would be:

  1. Machine code: Go
  2. Bytecode at Build time: Java, Kotlin, C#
  3. Bytecode at Runtime: Javascript, Python

What about JIT compilation? Java, Kotlin, C#, and 1TypeScript all use just-in-time compilers that compile bytecode to machine code at runtime, will this influence the results? Assumption: First requests should be slower, as we are interpreting code and using CPU time to compile code.

Benchmark#

Let’s benchmark it with following implementations:

LanguageFramework
TypeScriptFastify
PythonFastAPI
JavaSpring Boot
C#ASP.NET Core
GoChi
KotlinKtor

So I decided to run a benchmark on an environment that I know well and love. Serverless container in Google Cloud Run. I can deploy docker containers, you pay per request usage, it scales up and down automatically, it has a CPU boost at startup to avoid cold start.

I ran a simple scenario that does a series of CRUD operations in memory, it will tell me how fast each language is to respond to and handle HTTP requests.

As Benchmark runner I used 2k6, with a load of 50 Request per second, during 300 seconds, on 1 CPU, 512MB of RAM.

K6 Latency Benchmark

I was happily surprised that even for this simple load, everything was able to respond under 10ms 95% of the time. And 50% of the requests have a latency of 5ms or less, for all stacks. Yes, there are some flukes (probably JIT compiling) in some languages that I’m sure we can optimize or get around by tuning the specific ones. But my general first thought is that no matter if you are using Go or Python, the bottleneck is not in the pure language.

DEBUNKED: No matter if it is native code or byte code, if using 3JIT or 4AOT, everything runs under 10ms.

Digging deeper#

I decided to graph also the CPU used, and I can see clearly 3 groups.

K6 CPU Usage Benchmark Note: the down curve is when the benchmark stopped at 4:29PM.

  1. Go and Typescript: < 9% of CPU usage and steady.
  2. Java, Kotlin and C#: They start 20% or even 40% but quickly stabilize to < 14%
  3. Python: Steady at 21%.

Is this any indicator of where the bottleneck will be? Maybe yes, maybe it’s just how the different implementations of the in-memory storage work. Let’s continue our investigation, this alone isn’t enough.

🚚 Throughput#

Another assumption is “real applications spend most of their time doing I/O”: handling requests, reading from databases, making network requests, so does the speed even matter? For those workloads, which one is able to handle a lot of request in parallel?

The practical picture for the languages I’ve been comparing(more details):

LanguageFrameworkAsync ParadigmDB Async
TypeScriptFastifyEvent loop + async/awaitPrisma (native async)
PythonFastAPIAsyncIO + async defSQLAlchemy 2.0 async
GoChiGoroutines + context.Contextpgx (async driver)
JavaSpring BootThread pool + CompletableFutureJPA (blocking, sync)
KotlinKtorCoroutines + suspend funExposed (newSuspendedTransaction)
C#ASP.NET CoreTAP + async Task<T>EF Core async LINQ

My general assumption is that this should be roughly the podium:

  1. True Parallelism + Non-blocking I/O: Go and C#
  2. True Parallelism + Blocking I/O: Java and Kotlin
  3. Single execution context + Non-blocking I/O: TypeScript and Python

Benchmark#

Same machine in Cloud Run for this run, with a Postgres Cloud SQL instance (2 CPU, 8 GB RAM), that should be plenty to not be the bottleneck.

The Goal: How many request of CRUD operations the API can handle keeping response time under 300ms?

K6 Request Per Second Benchmark

Results:

  1. Go and C# handle 400 requests per second with a response time of 10ms or less 95% of the time.
  2. TypeScript and Java handle 300 requests per second with a response time of 10ms or less 95% of the time.
  3. Kotlin handles 250 requests per second with a response time of 12ms or less 95% of the time.
  4. Python handles 100 requests per second with a response time of 60ms or less 95% of the time.

Bumps on the road#

These results that you are seeing are not from the first run of the benchmark. I spent a lot of time trying to get Python at least on the podium, maybe the number of SQL queries was different from other implementations or the version of Python was not optimized for containers. So I upgraded from version 3.12 to 3.14, and nothing. I spent too much time reading about GIL and free threading, I’ll revisit this maybe in the future.

Then in my original run, C# was giving closer in the 200 Request per second, but the fix was easy, I upgraded from .NET 8 to .NET 10 and it did the trick.

The results from Kotlin were not bad, but a bit inconsistent between runs, I’m sure we can get it to the level of Java by reviewing a bit the implementation, I’ll look at this in the future.

Digging deeper#

Like for the Latency I started by looking at CPU usage.

K6 CPU Usage Benchmark

  1. Go at 50%
  2. Java, Kotlin, Python 60%
  3. C# and Typescript 70%-80%

I was expecting to be CPU bound around 60%, Cloud Run try to average CPU at 60% on 1 minute windows after that it will queue requests on the load balancer and wait for CPU to decrease or add more instances (disabled in our benchmark). C# and Typescript are maybe spiking over 60% but maybe not sustained. Go at 50% is not CPU bound but I was not able to get consistently higher values than 400 RPS, so the bottleneck seems to be elsewhere, if there is a way to squeeze a few more requests from it.

I continued by looking at the Response Time.

K6 Response Time Benchmark

Except Python and Kotlin, everything else had similar response time around 10ms, around 1-2ms slower than the in-memory version. It seems that our database was overkill with 2 CPUs and 8GB of RAM, even if preloaded with 10,000 rows, it acted more like a memory store than giving db perf, but it’s hard to isolate what you are testing.

So did our assumptions hold true?

  1. Go is first without a sweat, as expected — fast and good at parallelism. But C# .NET 10 being on par was a surprise.
  2. TypeScript and Java are a bit of a surprise here:
    • Java is using JDBC for database connections, meaning it blocks the thread where the request is running.
    • TypeScript is using a single-threaded event loop, but the I/O operations give back time to the scheduler, making it more efficient than expected.
  3. Kotlin — I was not expecting it to be here, at the same level or better than Java.
  4. Python — while I was expecting it to be last, not by that margin.

DEBUNKED: At least for non-Go results. Single-threaded is as fast as multi-threaded, at least on containers with 1 CPU. And blocking I/O can be as fast as non-blocking I/O. 🤯

Conclusion#

For latency, the language and the framework do not matter. For throughput, Go wins, but C#, TypeScript and Java are right behind it, which is the real surprise. Single-threaded and blocking I/O can both compete with true parallelism on a 1 CPU container.

Could using another TypeScript runtime like Node.js 24, Deno or Bun bring it even closer to Go? Probably! Could 4AOT compilation for C#, Java (GraalVM) or Kotlin Native reduce the cold start? Definitely!

And finally two outliers are the ones worth investigating next. Kotlin and Python couldn’t reach comparable throughput, but it’s not obvious why. Is it the 5GIL in Python’s case? Is it a container sizing issue? Is it the runtime itself? Before drawing conclusions about the language, those questions need answers.

If you want to dig into the current implementations, the full code is on GitHub.

What’s next?#

The next step is adding proper OpenTelemetry instrumentation to see exactly where the time is spent, how much is framework overhead, how much is query execution, how much is serialization. That should make the Kotlin and Python results explainable rather than just anomalous.

Beyond that, a few questions I want to explore:

  • Would a multi-CPU container have changed the results?
  • Does the throughput ranking hold when moving to a more complex workload joins, transactions?
  • Is the Java and Kotlin result repeatable with a non-blocking driver like R2DBC instead of JDBC?

Footnotes#

Footnotes#

  1. Every time I mention Typescript it will be as proxy to say Javascript. ↩

  2. https://k6.io/open-source/ ↩

  3. Just In Time ↩

  4. Ahead Of Time ↩ ↩2

  5. Global Interpreter Lock ↩