Node.js vs Rust performance for webservers
Tags: node, rust, performance, benchmark
Published: 2022-06-14 17:02:41

Disclaimer: This post aims to compare the performances between Node.js and Rust but only in the setup used here. No language is a fit for all your projects and more than raw performances should be considered when choosing a language for your applications.

## Out of the Javascript comfort zone

I've been using exclusively Javascript for the past years.
Usually, when I'm trying to optimise for backend performances, I would look into caching, code optimisations, DB perf, CDN, scaling…


But I've been wondering, how much gain could there possibly be if some of the microservices could be in a different language like Rust?
Let's find a project, create Node.js and a Rust backend and compare both.


## Web server: Url shortener
I think an URL shortener is a great fit for this test, the application would be a web server with one route, it receives a short-Id and lookup in a database to find the corresponding long URL to redirect to.

The architecture for the application is like this:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gp29m3lbc3uigwdu9fly.png)

Test conditions: Both Node.js and Rust web servers are using the same cloud infrastructure. both would be only one instance with the same Ram/CPU.

## rust code

I'm a beginner in rust but Rocket seems to be a suitable web server for this experience, also aws seems to have published a preview of their rust SDK.

```rust
#[macro_use] extern crate rocket;
use rocket::response::Redirect;
use rocket::http::Status;
use rocket::State;

struct AppState {
    client: aws_sdk_dynamodb::Client
}

#[get("/<id>")]
async fn get_redirect_url(id: &str, state: &State<AppState>) -> Result<Redirect, Status> {
    match state.client
        .get_item()
        .table_name("table_name")
        .key("id", aws_sdk_dynamodb::model::AttributeValue::S(id.to_string()))
        .send()
        .await
        {
            Ok(resp) => {
                match resp.item.unwrap_or_default().get("url") {
                    None => Err(Status::NotFound),
                    Some(url) => Ok(Redirect::to(url.as_s().unwrap().clone()))
                }
            }
            Err(_e) =>  Err(Status::InternalServerError)
        }

}

#[launch]
async fn rocket() -> _ {
    let shared_config = aws_config::load_from_env().await;
    let client = aws_sdk_dynamodb::Client::new(&shared_config);

    rocket::build().manage(AppState { client: client }).mount("/", routes![get_redirect_url, health])
}
```

## Node.js code

Same code in Node.js, fastify seems to have better performance than express, so let's keep all the chances on our side and use it.

```js
const fastify = require('fastify')()
const AWS = require('aws-sdk');

const dynamo = new AWS.DynamoDB.DocumentClient();

fastify.get('/:routeId', async (request, reply) => {
    const {Item} = await dynamo.get({ 
        TableName: "table_name",
        Key: {
            "id": request.params.routeId,
        }
    }).promise();

    if (Item) {
        reply.redirect(303, Item.url)
    } else {
        reply.code(404).type('text/html').send('Not Found')
    }

})

const start = async () => {
  try {
    console.log("starting fastify");
    await fastify.listen({
        port: 8000,
    })
  } catch (err) {
    fastify.log.error(err)
    process.exit(1)
  }
}
start()
```
## performances test both our applications

both applications are now running on the aws cloud and ready to be tested, [k6](https://k6.io/) seems to be a great load testing tool. bonus point the config files are in Javascript.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8fqttf6jpaeil37lfai7.png)
This is the average duration for a user to make a request and get redirected (tested in similar conditions on different network types)
While rust is a winner, 45ms for an URL shortener request is really great. Not enough to get me out of my JS comfort zone.


Now let's get into serious things and load tests with multiple users at the same time.
load test setup:

- 50 virtual users
- 30 seconds experience duration


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sbhquswtkk9gav3vdgak.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/97wp5ke64eksfz7wo313.png)

Huge win for rust! On higher traffic, the node.js application did not deliver as many requests, and the high request duration time would result in a poor experience for users.
With auto-scaling on the application server, node.js could have kept the sub-50ms response time but this would mean a higher cost.

_Isn't that an unfair comparison, rust web framework uses multiple threads while node.js is single-threaded?_
It's true that while on high load node.js CPU was not 100% of its capacity (between 30 and 50%), but even if we switch the code to use multiple threads, this would not have been enough. Additionally everywhere I've worked, we always used single thread node.js application on production.

## Conclusion

While Javascript will probably stay my language of choice for web backend due to my experiences with it and all the great tools in the ecosystem, I would definitely consider using rust in small and high-volume microservices!