Node.js: The fastest web framework in 2025 — Static file server case
Introduction
This article serves as the 2025 update to one of my most widely read pieces over the past three years. At the start of 2024, I published a comparative analysis of the most popular web frameworks in the Node.js ecosystem. That article & the series continues to attract significant traffic and remains the top result on Google for its topic.
In the previous & first article of this series, we’ve already seen how the frameworks compare to each other for the simplest hello world case. The results (RPS only) are summarized below:
From this article, we’ll start looking at more practical use cases. In this article, the second in the series, we’ll take a comparative look at the case of static file servers.
NOTE: The winner from ‘hello world case’, hyper express, is dropped out of this comparison. HyperExpress uses LiveDirectory to load all the files in memory, which makes it impractical. I have 100K files in my test environment, and loading them into memory is totally impractical.
If you happen to see this article first, you can see others in this series here:
Test setup
All tests are executed on MacBook Pro M2 with 16G RAM. There are 8+4 CPU cores. The Node.js version is v23.11.1 (latest at the time of writing).
The load tester is a modified version of Bombardier (written in Go) that can send a different static URL for each request.
There are 100K files in the static directory. Each file size is exactly 10K (102400 bytes).
The code for each application is as follows:
Express
import express from "express";
const app = express();
app.use(
"/static",
express.static("/Users/mayankc/Work/source/perfComparisons/static"),
);
app.listen(3000);
Fastify
const fastify = require("fastify");
const app = fastify();
app.register(require("@fastify/static"), {
root: "/Users/mayankc/Work/source/perfComparisons/static",
prefix: "/static/",
prefixAvoidTrailingSlash: true,
});
app.listen({ port: 3000 });
Hapi
const Hapi = require("@hapi/hapi");
const start = async () => {
const server = Hapi.server({
routes: {
files: {
relativeTo: "/Users/mayankc/Work/source/perfComparisons/static",
},
},
port: 3000,
host: "127.0.0.1",
});
await server.register(require("@hapi/inert"));
server.route({
method: "GET",
path: "/static/{filename}",
handler: {
file: function (request) {
return request.params.filename;
},
},
});
await server.start();
};
start();
Koa
import Koa from "koa";
import serve from "koa-static";
import mount from "koa-mount";
const app = new Koa();
app.use(
mount("/static", serve("/Users/mayankc/Work/source/perfComparisons/static")),
);
app.listen(3000);
Restify
import restify from "restify";
var server = restify.createServer();
server.get(
"/static/*",
restify.plugins.serveStaticFiles(
"/Users/mayankc/Work/source/perfComparisons/static",
),
);
server.listen(3000);
Ultimate Express
import express from "ultimate-express";
const app = express();
app.use(
"/static",
express.static("/Users/mayankc/Work/source/perfComparisons/static"),
);
app.listen(3000);
NestJS
main.ts
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();
app.module.ts
import { Module } from "@nestjs/common";
import { ServeStaticModule } from "@nestjs/serve-static";
import { AppController } from "./app.controller";
@Module({
imports: [
ServeStaticModule.forRoot({
rootPath: "/Users/mayankc/Work/source/perfComparisons/static",
serveRoot: "/static",
}),
],
controllers: [AppController],
providers: [],
})
export class AppModule {}
app.controller.ts
import { Controller, Get } from "@nestjs/common";
@Controller()
export class AppController {
constructor() {}
@Get()
getHello(): string {
return "Hello, world!";
}
}
NestJS Fastify
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(),
);
app.useStaticAssets({
root: "/Users/mayankc/Work/source/perfComparisons/static",
prefix: '/static/',
});
await app.listen(3000);
}
bootstrap();
app.module.ts
import { Module } from "@nestjs/common";
import { AppController } from "./app.controller";
@Module({
imports: [],
controllers: [AppController],
providers: [],
})
export class AppModule {}
app.controller.ts
import { Controller, Get } from "@nestjs/common";
@Controller()
export class AppController {
constructor() {}
@Get()
getHello(): string {
return "Hello, world!";
}
}
Results
A total of 200K requests are executed for 100 concurrent connections. Along with latencies, 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:
Verdict
To begin with, the results for a practical scenario like serving static files differ significantly from the theoretical “Hello, world” benchmark. In this test, the requests per second (RPS) for most frameworks fall within a relatively narrow range of 8,000 to 9,500, indicating minimal performance variance across the board.
Notably, Express, which ranked lowest in the previous benchmark, climbs to second place in this round. This improvement may reflect the benefits of its long-standing maturity and ongoing optimizations over the years.
The top performer in this category is Restify, which delivers the highest throughput while maintaining relatively low resource consumption — making it a strong candidate for static content delivery.
Interestingly, Ultimate-Express, which led the previous round, ranks at the bottom this time, highlighting how performance can vary significantly depending on the nature of the workload.
Winner: Restify
If you happen to see this article first, you can see others in this series here: