Sitemap

Java vs. Node.js Performance: A Comparative Analysis

5 min readSep 12, 2024
Press enter or click to view image in full size

Introduction

Node.js or Java — Which Should You Choose for a new microservice ?

When deciding between Node.js and Java for a new microservice, several critical questions arise:

  • Will Java scale effectively with growing demands?
  • Can NodeJS handle CPU-bound tasks efficiently?
  • How do these two technologies compare in terms of performance?

To answer these questions, it’s essential to set up test scenarios that evaluate both technologies under typical workloads. Here’s how I approached it:

Test Scenarios:

  1. CPU-Bound Test: Evaluates how well each technology handles tasks that require intensive CPU processing.
  2. I/O-Bound Test: Assesses the performance of each technology when dealing with operations like file handling, database queries, or network requests.

By running these tests, we can compare the strengths and weaknesses of Node.js and Java in real-world situations, helping to make an informed decision for the microservice.

CPU Bound Test Case

NodeJS

const secondsToWait = 1;

@Post('/cpu-bound')
async cpubound(): Promise<any> {
const waitTime = secondsToWait * 1000000;
const startTime = process.hrtime.bigint();
while (process.hrtime.bigint() - startTime < BigInt(waitTime)) {
// waiting utilizing cpu, incresing cpu usage.
}
return {status: "ok"};
}

Java

int secondsToWait = 1;

@PostMapping("/cpu-bound")
public ResponseEntity<?> cpuBound() {
long waitTime = secondsToWait * 1000000L;
long startTime = System.nanoTime();
while ((System.nanoTime() - startTime) < waitTime) {
// waiting utilizing cpu, incresing cpu usage.
}
return ResponseEntity.ok("{\"status\": \"ok\" }");
}

IO Bound Test

NodeJS

function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}

const secondsToWait = 1;

@Post('/io-bound')
async iobound(): Promise<any> {
// waiting without utilizing cpu
// io wait, operation is performing is some other system
await sleep(secondsToWait * 1000);
return {status: "ok"};
}

Java

int secondsToWait = 1;

@PostMapping("/io-bound")
public ResponseEntity<?> ioBound() throws Exception {
// waiting without utilizing cpu
// io wait, operation is performing is some other system
Thread.sleep(secondsToWait * 1000);
return ResponseEntity.ok("{\"status\": \"ok\" }");
}

Once we have pinned down our test scenarios, we can start testing, to test these APIs I am going to use K6 load testing tool. Check it out https://k6.io/

K6 — Testing Script

import http from 'k6/http';
import {check} from 'k6';

export const options = {
vus: 1,
duration: '1s',
thresholds: {
// http errors can be max 5%
http_req_failed: ['rate<0.06'],
}
};

const BASE_URL = `http://localhost:${PORT}`;

export default function () {
const headers = {};
const body = {};

let url = `${BASE_URL}/api/v1/cpu-bound`
if (__ENV.TEST_TYPE === "IO") {
// change url when testing io
url = `${BASE_URL}/api/v1/io-bound`
}
const response = http.post(url, body, {headers});

check(response, {
// check the response should be 200
'is status 200': (r) => r.status === 200 || r.status === 201,
// check the response body should contain status as ok
'token is present': (r) => {
const res = r.json()
return res.status && res.status.length > 0 && res.status === 'ok'
},
});
}

Start Testing : 4 GB Memory & 8 vCPUs

CPU Bound — Java

Running k6, puts load on the server, I am putting 150 virtual/parallel users, and each user would be using the server for at least 30 seconds.

k6 run .\script-java.js --vus=150 --duration=30s
Press enter or click to view image in full size
cpu: 100 % 
memory: 2 GB
http: 18 req/s
// In Java Spring boot one threads is used for each request that comes
// and context switching is done to achive parallism

CPU Bound — NodeJS

k6 run .\script-node.js --vus=150 --duration=30s
Press enter or click to view image in full size
cpu: 100 % 
memory: ~ 1 GB
http: 1 req/s
// node js is single threaded with a event loop
// and the test case wait for 1 sec busy wait
// thus only 1 req is process at a time per second

CPU Bound — NodeJS ( Clustering )

In NodeJS, we can use the cluster module to create multiple NodeJS processes, each listening on the same TCP port. This allows the application to handle multiple TCP connections concurrently. The operating system acts as a load balancer, distributing incoming requests across the different Node worker processes, typically using a round-robin approach. This setup takes advantage of multi-core processors, improving the scalability and performance of the application.

async function main() {
const numCPUs = os.cpus().length;
console.log(`${numCPUs} cpu cores found`);
if (cluster.isPrimary) {
console.log(`Primary process(${process.pid}) started`);
// start 1 process per cpu core using fork
for (let i = 0; i < numCPUs; i++) {
// creates a new node process
// which starts executing the same script
// but cluster.isPrimary is false for worker
const worker = cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
console.log(`worker process(${worker.process.pid}) exited with code ${code} and ${signal}`);
});
} else {
// Workers can share any TCP connection
// In this case it is an HTTP server
startServer(8080);

console.log(`Worker process(${process.pid}) started`);
}
}

Testing again

k6 run .\script-node.js --vus=150 --duration=30s
Press enter or click to view image in full size
cpu: 100 % 
memory: ~ 2 GB
http: 8 req/s
// 8 process for 8 vCPUs serving 1 req/s, thus 8 req/s utilizing all cores
// context switching between process is expensive,
// In java which is with threads is better at serving cpu bound requests

IO Bound — Java

In I/O-bound tests, the performance of the server is primarily limited by the speed of disk access, network operations, or database queries, rather than CPU processing power. When the number of parallel users (VUs) is too low, the server might handle the load efficiently, leading to similar results. Thus increasing the parallel user helps.

k6 run .\script-java.js --vus=1000 --duration=10s -e TEST_TYPE=io
Press enter or click to view image in full size
cpu: ~ 60 - 70 % 
memory: ~ 4 GB
https: 190 req/s

IO Bound — NodeJS

k6 run .\script-node.js --vus=1000 --duration=10s -e TEST_TYPE=io
Press enter or click to view image in full size
cpu: ~ 45 % 
memory: ~ 2 GB
http: 804
// clearly node js is much better at handling io bound request

IO Bound — NodeJS ( Clustering )

k6 run .\script-java.js --vus=1000 --duration=10s -e TEST_TYPE=io
Press enter or click to view image in full size
cpu: ~ 70 % 
memory: ~ 3 GB
http: 951
// clustering improves the node js performance even better
Press enter or click to view image in full size

Conclusion

The performance comparison between Java and Node.js reveals distinct strengths and weaknesses in handling different types of workloads.

--

--