007

Java, Python, Node and Rust on the same REST service: 7 MB to 191 MB at idle

At idle the Rust service uses 7 MB of memory, Node 80 MB, Python 85 MB and Java 191 MB. Four implementations of one REST service run on Minikube with Prometheus and Grafana, and the project runs with one command.

problem

Moving a service to another language was expensive because of the integration work. The language itself was easy to learn. You had to choose a framework, an ORM, migrations and metrics, make them work together and connect them to the production tooling. AI coding tools now do much of that work, so the runtime choice is worth measuring again.

When a team considers a new language, the language is rarely the problem. The work is in the stack around it: a framework, an ORM, an HTTP server, a migration tool and metrics, all configured to work together in production. Documentation is often out of date, and it can take days before the first request works.

This integration cost stopped most migration discussions early. AI coding agents reduce it, so it makes sense to measure the runtimes again.


What AI changes

In a company, AI does not replace the team, design the architecture or decide a migration. It is good at integration work: it puts frameworks together, resolves dependencies, pins versions and writes working configuration. This work was the main cost of adopting a new runtime.

What is left is the question of whether another runtime is worth the change. The measurements below help to answer it.


The benchmark setup

Four microservices have the same REST endpoints, use PostgreSQL and export Prometheus metrics. They implement the same CPU-heavy business logic, so the differences under load come from the runtime.

The four services run on one Minikube cluster and share one PostgreSQL instance, with one schema per service. Prometheus scrapes all four services. Grafana loads a comparison dashboard at startup. make seed sends 500 orders to each service in parallel, across all discount tiers. That is enough load for the metrics without a separate load-testing tool.


Stack overview

java-service Spring Boot 3.4
python-service FastAPI
node-service Fastify 4
rust-service Axum 0.7
port
8081
8082
8083
8084
ORM
Spring Data JPA + Hibernate
SQLAlchemy 2.x async + asyncpg
Prisma (schema: node_bench)
SQLx with postgres feature
migrations
Flyway (schema: java_bench)
Alembic (search_path: python_bench)
Prisma migrate deploy (on startup)
sqlx::migrate! embedded (on startup)
metrics
Micrometer + /actuator/prometheus
prometheus-fastapi-instrumentator
prom-client with custom middleware
metrics + metrics-exporter-prometheus
runtime
eclipse-temurin:21-jre
python:3.12-slim
node:20-slim (multi-stage)
debian:bookworm-slim (statically linked)

Measurements

CPU under load

Java shows the usual JIT warmup curve. CPU goes up while the JIT profiles and compiles the code, then it drops below Python and Node. Rust stays low and flat from the first request. Python and Node use the most CPU during the whole run.

cpu usage per service · % during seed load
50% 40% 30% 20% 10% 0% 11:53 11:54 11:55 11:56 11:57
node.js 21.1% python 15.7% java 8.36% rust 4.36%

Python and Node run each iteration in an interpreter or a JIT with boxing, dynamic dispatch and garbage collection. Rust compiles to native code. After warmup, Java is closer to Rust than to Python and Node. Python could use C extensions such as NumPy to reduce the gap. Here the logic is plain Python, so the chart shows the cost of the runtime itself.

Memory

Under load, Java, Python and Node use 10 to 20% more memory than at idle. Rust memory stays the same.

resident memory at idle · baseline = rust
rust
7.34 MBbaseline
node
~80 MB×11
python
~85 MB×12
java
~191 MB×26
process RSS before any seed traffic · lower is better

Rust allocates only the memory the service uses. Node and Python load an interpreter and all imported modules. Java starts a virtual machine, a JIT compiler, the Spring context and the Hibernate metamodel before the first request. The Rust service is one statically linked binary of a few megabytes.

Many servicesWith 30 services, a memory ratio of 11x to 26x changes the number of cluster nodes and the monthly bill. The saving is worth a rewrite only if it is larger than the cost of the migration.

Artifact size

The Rust binary is a few MB, but the image uses debian:bookworm-slim as base and is 116 MB. The Python image on python:3.12-slim is 243 MB. The Java and Node images are about 400 MB each. In Java most of the size is the JRE layer under the fat jar. In Node it is node_modules and the Prisma query engine.

container image size · baseline = rust
rust
116 MBbaseline
python
243 MB×2.1
java
399 MB×3.4
node
408 MB×3.5
uncompressed image on disk · lower is better

What the benchmark does not capture

The benchmark measures the runtimes. A migration decision also depends on the systems the service uses: authentication, internal services, data contracts, observability and the deployment model. With lower integration cost, you can build a working prototype and compare it with the migration estimate.

Some technologies are chosen for their libraries. AI tools cannot generate a replacement for a mature library such as a large Apache project. For standard services, with a repository layer, an API layer and common integrations, the runtimes are now close to interchangeable, and a migration is a normal engineering trade-off.


When a migration makes sense

It makes sense when the service does not depend on libraries that exist only in one ecosystem, and the target runtime has a mature framework for the job. A REST API on a relational database with standard observability is this case. Spring Boot, FastAPI, Fastify and Axum all cover it. In this benchmark the Rust service used less CPU than Python and Node, and less memory than all the others.

It does not make sense when the service depends on libraries of one runtime. Python ML inference depends on PyTorch, NumPy or JAX, and these libraries have no production-ready equivalent outside CPython. Agent systems built on LangGraph have the same limit, because LangGraph and its integrations are Python libraries. Services with a lot of business logic are also bad candidates. Rewriting the business logic costs more than the runtime saves, and AI tools reduce the integration work, not this rewrite.


Run it yourself

The project is at github.com/ValerioMC/runtime-bench. It includes the four services, the Kubernetes manifests, the Prometheus scrape configuration and the Grafana dashboard.

You need Docker, Minikube and kubectl. One command creates the cluster, builds the images, deploys the services and starts the port-forwards:

make up

make stress then sends concurrent load to the four services for 120 seconds, with 8 workers per service. You can change the duration and the concurrency:

make stress                     # 120 s default
DURATION=300 CONCURRENCY=16 make stress

Grafana runs at http://localhost:3000 with user admin and password admin. Its dashboard shows CPU, memory and request throughput of the four services side by side. The charts in this article come from that dashboard.