Spring Boot Webflux vs Node.js frameworks: Performance comparison for hello world case
This is a requested article.
This article compares Spring Boot Webflux 3.2 with all popular frameworks on the Node.js side.
Note: Some of the Node frameworks are very basic, while some are feature rich. We’ll still compare with all of them.
Test setup
All tests are executed on MacBook Pro M2 with 16G RAM & 8+4 CPU cores. The load tester is Bombardier (Written in Go). The software versions are:
- Java 21
- Spring Boot Webflux 3.2.1
- Node.js v21.5.0
The application code is as follows:
Java
application.properties
server.port=3000
spring.threads.virtual.enabled=true
Application.java
package hello;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.web.reactive.config.EnableWebFlux;
import org.reactivestreams.Publisher;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import reactor.core.publisher.Mono;
@SpringBootApplication
@EnableWebFlux
@EnableAsync
@Controller
public class Application {
public static void main(String[] args) throws Exception {
SpringApplication.run(Application.class);
}
@GetMapping("/")
@ResponseBody
public Publisher<String> handler() {
return Mono.just("Hello world!");
}
}
Node.js
Express
(Etag is disabled because other frameworks doesn’t send it by default)
import express from "express";
const app = express();
const reqHandler = (req, res) => {
res.send("Hello World!");
};
app.disable("etag");
app.get("/", reqHandler);
app.listen(3000, () => console.log("Listening on 3000"));
Fastify
import Fastify from "fastify";
const fastify = Fastify({
logger: false,
});
const reqHandler = (request, reply) => {
reply.send("Hello World!");
};
fastify.get("/", reqHandler);
fastify.listen({ port: 3000 });
Koa
import Koa from "koa";
import Router from "@koa/router";
const app = new Koa();
const router = new Router();
const reqHandler = (ctx, next) => {
ctx.body = "Hello World!";
};
router.get("/", reqHandler);
app
.use(router.routes())
.use(router.allowedMethods());
app.listen(3000);
Hapi
import Hapi from "@hapi/hapi";
const server = Hapi.server({
port: 3000,
host: "127.0.0.1",
});
const reqHandler = (request, h) => {
return "Hello World!";
};
server.route({
method: "GET",
path: "/",
handler: reqHandler,
});
server.start();
Restify
import restify from "restify";
const reqHandler = (req, res, next) => {
res.contentType = "text/plain";
res.send("Hello World!");
next();
};
var server = restify.createServer();
server.get("/", reqHandler);
server.listen(3000);
HyperExpress
import HyperExpress from "hyper-express";
const webserver = new HyperExpress.Server();
const reqHandler = (request, response) => {
response.send("Hello World!");
};
webserver.get("/", reqHandler);
webserver.listen(3000);
NestJS
(The NestJS app code is generated through their CLI. Only few relevant files are shown here)
main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.getHttpAdapter().getInstance().set('etag', false);
await app.listen(3000);
}
bootstrap();
app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
app.controller.ts
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}
app.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}
NestJS (Fastify)
By default, NestJS uses express adapter. Therefore, the performance is slightly worse than Express (which itself is the slowest in this lot). Fortunately, NestJS allows configuring fastify adapter instead of express.
main.ts
import { NestFactory } from "@nestjs/core";
import {
FastifyAdapter,
NestFastifyApplication,
} from "@nestjs/platform-fastify";
import { AppModule } from "./app.module";
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter(),
);
await app.listen(3000);
}
bootstrap();
All other files are same as NestJS (express).
AdonisJS
(The AdonisJS app code is generated through their CLI. Only few relevant files are shown here)
server.ts
import 'reflect-metadata'
import sourceMapSupport from 'source-map-support'
import { Ignitor } from '@adonisjs/core/build/standalone'
sourceMapSupport.install({ handleUncaughtExceptions: false })
new Ignitor(__dirname)
.httpServer()
.start()
routes.ts
import Route from '@ioc:Adonis/Core/Route'
Route.get('/', async () => {
return "Hello World!"
})
Results
A total of 5M requests are executed for 100 concurrent connections. Along with latency, I’ve also collected CPU and memory usage. This gives a complete picture. Just having high RPS is not enough. We also need to know the cost of the RPS.
The results in chart form are as follows:
Time taken & RPS
Latencies
Resource usage
Impressions
Spring Boot Webflux is an extensive, feature-rich framework that offers numerous things out of the box. Comparatively, on the Node side, the closest competitor is NestJS. Others like Express, Fastify, Restify, Hapi, etc. are way too small compared to Webflux.
From the performance point of view, Spring Webflux gets beaten only by one framework: HyperExpress. On the resource front, Webflux’s resource usage is very high compared to Node frameworks.
Thanks for reading this article!