Recently this video from Dave’s Garage on Youtube popped up in my feed.
He makes a valid point about your current super awesome killer PC being slower than the 10year old one it just replaced. Not because new hardware sucks, on the contrary new hardware is leaps and bounds faster and in some cases even lighter on the watts, so that you can have some semblance of a hope your Windows laptop can compete with the battery life of the Hipster’s Mac on the other side of the coffee shop. Rather that the software that runs on it requires more and more from the hardware, and maybe even to the point where the software devs just become lazy and say, I’m not optimizing this routine, someone can just throw hardware at it if they don’t like how slow it is.
On the OS level:
If you look at how much RAM your Android phone needs to be responsive, then: Galaxy 2 1GB, 2xCores, now: Galaxy 25 12/16GB, 8xCores. Even that feeling you get when you upgrade your 7year old laptop from Windows 10 to 11 and then realizing what a big mistake it was. And don’t get me started on how getting a new Laptop with an i3 and 8GB of RAM running Windows 11, feels like having to browse Youtube over dial-up internet.
On the Framework level:
System frameworks are pretty much the same, they are like the Victorinox Swiss Champ XXL . They contain everything you might ever need to build any web project, from REST, MVC, MVP, middleware, security. database stuff, etc. None of which is bad, but if I just want to build a simple site with a submit button, I don’t need 98% of that, however now my app server needs 500Mb for the framework and takes 15s to start just to host this simple site.
The Plan
I find Testing OSes to be boring and not easy to achieve and do correctly or objectively. On the other hand to test languages and recommended frameworks is a lot easier and potentially less boring. To test these frameworks “equally” I will require all of them to solve the same problem. Since most of my daily work happens in the Fintech space I decided to ask an LLM for a simplified approximation of a FICO score. So there are 6 inputs, that are fetched from a db, some calcs on these inputs happen and in the end a score pops out. Each of the tested implementations expose the same REST like API and returns the same JSON output.
curl http://localhost:8080/user/3452
{"Id":3452,"Name":"Emmaline Whittaker","Score":623}
The platforms that have been identified are, java, .net, golang, typescript and python. Why these? Well I asked two different LLMs the same thing:
Give me 5 languages recommended to build REST APIs in, based on ease of
implementation, production stability, community support and security.
I also approach each language with the normal vibe coding AI crutch jnr devs will use. I asked the LLM to recommend the fastest and most standard way to build such an API in the language and recommended a framework. I also asked for the easiest and most standard high performant API server, an ORM that doesn’t suck and a connection pool for the ORM. I will also ask for the most optimized production ready way to run it. Each framework will be tested using k6 using 5 concurrent connections, and because connection pools are weird we will ask for 15 initial and 50 total, if we can.
My expectations are high, I’m C/C++ trained and have been forced by the industry to move to the lazy languages, because only boomers and neck beards code in C/C++ these days, apparently. So my really fragile, not production ready, I’m-super-rusty-in-C demo app, before forking, uses 8Mb RSS and startup is instant.
C the baseline
Since the c app doesn’t support multiple concurrent connection, it was changed to fork 5 children that process the requests. The parent uses 6Mb and each child uses 8Mb that is why the memory on the chart is 48Mb. The memory “spikes” with 600kb during warmup. During the main run we use 1 core and takes 20s.

Java
Ok so the the trendy results for Java selected the following:
- Springboot - Framework that provides API and DB integration
- Hybernate - ORM
- gradle - Builder
Welcome to the Jungle
If you are new to java the code is structured in a military fashion and there is A LOT of things in different places. There is a file for startup, a file for the controller a file for the service, etc. There is also dependency injection where the framework constructs your classes as singletons and then injects them automagically.

Does it care?
To run java applications you apparently need to type and entire novel to run it optimally.
java -server -Xms64m -Xmx2g -XX:MinHeapFreeRatio=20 -XX:MaxHeapFreeRatio=40
-XX:+UseG1GC -XX:+UnlockDiagnosticVMOptions -XX:+DisableExplicitGC
-XX:+AlwaysPreTouch -jar ./build/libs/java-orm-0.0.1-SNAPSHOT.jar
Out of the gate the memory usage for this abomination is ~290MB and startup time is 3s. The memory spikes to ~380MB during the warmups and then again to ~410MB during the main run. It uses 3cores and took 38s to complete the task. Interestingly there is 0 CPU i/o wait.

This is a bit crazy, 410MB for a simple API. Let’s close the taps on the heap (-Xmx) to a “max” of 256MB and see what impact that has on performance.
java -server -Xms64m -Xmx256m -Xss512k -XX:MaxMetaspaceSize=96m
-XX:+AlwaysPreTouch -jar ./build/libs/java-orm-0.0.1-SNAPSHOT.jar
Out of the gate it ignores the cap and the memory usage, performance remain mostly the same.

.Net - c#
Ok so the the trendy results for c# selected the following:
- .Net Core - Framework that provides API and DB integration
- Entity Framework - ORM
- msbuild - Builder
Still a jungle
If you are new to c# the code is structured in a logical fashion and there are a couple of things in different places. There is a file for startup, a file for the controller, etc. There is also dependency injection. Looks and feels very much like java.

Does it care?
To run it optimally we just have run the following:
dotnet run -c Release
Out of the gate the memory used is 75MB and startup is 2s. We can see the memory spikes to 175MB during the warmups and then drops down to ~170MB during the main run. The CPU usage is 5cores. The processing time however is 1m03s.

Deno - js
Ok so the trendy results for deno suggested the following:
- Built-in Http Server - API
- No ORM was suggested
- deno - Build tool
Where are all my files at
Minimalistic, only 2 files. One for the endpoint, setup and data fetching. Feels very strange after the strict structure of c# and java.

Does it care?
To run it optimally you can compile and run a binary.
deno compile --allow-env --allow-net --output server server.ts
./server
Out of gate the memory usage is 380MB and startup is <1s. We can see that memory spikes to 410MB during the warmups and then spikes to 480MB during the main run. With the single-threaded event driven design of deno and node.js before it, it only uses 1core. The processing time however is 48s, which is pretty good for one little thread.

golang
Ok so the trendy results for go suggested the following:
- Built-in Http Server - API
- gorm - ORM
- go - builder
More than 2 files
Minimalistic, not as little as deno, but still one main file and style recommendations of splitting things into their own go files, to make navigation easier.

Does it care?
To run it optimally you need to compile a chunky binary and run it.
go build -o go-orm *.go
./go-orm
Out of the gate we notice memory usage is less than 11MB and startup is instant. We also notice memory spike to 17MB during the warmups, and spikes again to 19MB during the full run. The CPU usage is 2cores. The processing time is 26s.

Python
OK so the trendy results for python suggested the following:
- fastapi + uvicorn - for the API
- No orm
- pipx - for dependencies and running
One file wonder
Minimalistic, just one file. Not sure why the LLM recommended this approach, surely we can split the server/main from the user model.

Does it care?
To run it optimally when you are lazy and your os is pip locked use pipx.
pipx run server.py
Out of the gate it uses 55MB of memory and startup is 2s. Memory usage doesn’t spike either during warmup or the main loop. The CPU usage is 1 core. Processing time is a dream shattering 4m51s.

The performance sucks so badly, let’s stop being lazy and make the required performance upgrades. That requires switching pipx out with granian and forking 5 workers like the C app.
To run it optimally
granian --interface asgi --workers 5 --runtime-threads 2
--no-access-log --port 8080 --backlog 2048 server2:app
Out of the gate the memory usage is 324MB and startup is 3s. Memory usage spikes to 330MB during warmup and then down to 326Mb during the main run. CPU usage is 4cores. Processing time is better at 1m35s, but not wow.

Final thoughts
Let’s plot our findings. Below we plot the 3 things that we measured that are important, CPU usage (cores), Memory usage and processing time. Clearly the one that cares the most is the one that has the lowest CPU, lowest memory usage and fastest processing time, however things are not always as perfect as you want them to be and you have to make trade-offs.

Looking at the rest Java and Deno used the most amount of Memory, and C# used the most CPU, however the performance gain from the 5cores, was worse than deno with 1core. If you looked at just CPU and Memory, python looks like a solid contender, but then you start caring about speed and you see it really sucked in the benchmark. This is a very narrow scoped “service”, so we can’t make generalized judgments about the languages and their frameworks. I don’t even want to list some take-aways from this article.
Development does not have one language that is the best for every problem, because of that we have thousands of programming languages each that does something different to the others. Sadly most companies brand themselves as a C#/Java shop and attempt to solve all the problems using them. It is always sad to see a big linux server in prod that only runs 1 jar, and nothing else, because the workload requires that jar to consume all 32GB of RAM. In my opinion you can probably scale that server down to only have 2GB of RAM if you were to convert the Java to C, however I would much rather maintain Java source than the mountains of C source. Having said that I would rather work with C pointers than with Java generics. But all this boils down to the squeezing the balloon metaphor. If you want an easy development experience writing minimal code, you squeeze the balloon, and what you don’t write is now part of the framework and you relinquish control over speed, performance, security, features and memory usage. And the opposite is also true, if you want to claw back either speed, performance or memory usage you either have to take control away from the framework, remove the framework entirely or rewrite it in a completely different language. Python also has balloon squeezing, however here you sacrifice speed for the enormous eco system and productivity gains, just look at those train-my-own-ai samples online 10 lines of python starts training your own Convolutional Neural Network.
I started off by saying nobody cares anymore, and that is not entirely true, C never stopped caring we just became lazy, and golang the man in the middle attack on C and C++ also deeply cares. So the next time you start a new project just ask yourself, in this day and age of the great RAM shortage: Do I even care if we can even afford another 500Mb Java “micro” service? Or should you pivot to something more green like golang.
Test setup
K6
K6 was run using a simple script that selects a random user by id between 0 - 500,000, that is then sent to the current API under test. Each api gets 3 warm up runs of 5 concurrent users requesting 500 responses before the big test that has 5 concurrent users and 750,000 responses. K6 uses the custom bench/script.js file and was run as follows:
k6 run --vus 5 --iterations 750000 script.js
pidstat
pidstat was used to record the stats for the pid of the server, after which the results were sent through an ai-slop plot.py script that generated the resource graphs. The two projects that forked children record the pids for all the children and then an aggregate file is made running plothelp.py before plotting the data with plot.py. All the scripts and recorded raw and aggregate stats are in the bench
folder.
pidstat was run as follows:
pidstat -h -r -u -p 204909 1 > /tmp/stats
System:
- Versions of toolchains:
- java
- openjdk-21-jdk-headless:amd64
- javac 21.0.10
- gradle 9.3.1
- golang
- go version go1.25.5 linux/amd64
- dotnet
- 10.0.107
- js
- deno 2.6.9 (stable, release, x86_64-unknown-linux-gnu)
- v8 14.5.201.2-rusty
- typescript 5.9.2
- k6
- v1.7.1 (commit/9f82e6f1fc, go1.26.1, linux/amd64)
- java
- OS
- Linux Mint 22.3
- CPU
- AMD Ryzen 7 - 8 cores
- DB
- postgresql 16.11-0ubuntu0.24.04.1
DB
The database creation and seeding is done by a go app in the db folder, and seeds a table called user.
create table user(
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name TEXT NOT NULL,
payment_history_percent_on_time REAL,
credit_utilization_ratio REAL,
credit_age_years INT,
total_accounts INT,
recent_inquiries INT,
derogatory_marks INT
);
Code
All the code samples are hosted on github