Back to Knowledge Base

Is Non-Blocking Nature of Node Still Relevant?

Sep 18, 2024
KK
Shivam MathurSenior Developer

Introduction

NodeJS has long been celebrated for its non-blocking I/O, a feature that allowed it to handle multiple requests efficiently without waiting for I/O operations to complete. This capability made it an attractive option for developers building high-performance web applications. However, with many programming languages now incorporating asynchronous features and futures, it's worth questioning whether NodeJS still holds a significant edge in this domain.

Generated by DALL-E 3

Experiment Setup

To explore this, I conducted an experiment comparing the performance of a basic web server implemented in NodeJS using Express and in Rust using Axum. Both servers exposed a single REST API endpoint that interacted with a PostgreSQL database, returning data from a single table with one record and no index.

The primary advantage of non-blocking I/O is that it allows a server to process other HTTP requests while waiting for the database to respond. This is precisely where NodeJS is expected to excel and why I initially chose NodeJS early in my career.

I used Apache Bench (ab) to benchmark both servers with 10,000 requests, running 1,000 requests concurrently. The tests were conducted on the following setup:

  • Machine: Lowest dedicated Linode (4GB RAM, 2 CPU cores, 80GB storage)
  • Database: PostgreSQL
  • Environment: Database, servers, and client all on the same machine to eliminate network latency

The code and setup for this experiment can be found here.

This experiment was conducted using the minimum dependencies required to set up and run the servers, focusing on a direct language-to-language comparison. It did not include a full suite of possible optimizations for either language.

Results

Errors

NodeJS:

  • NodeJS encountered errors when handling 1,000 concurrent requests, often breaking with the following error:
apr_socket_recv: Connection reset by peer (104) 
Total of 9578 requests completed
  • It managed to handle 500 concurrent requests without errors.

Rust:

  • Rust handled all requests without any errors.

Memory Usage

NodeJS:

  • Initial memory usage: 50-70MB
  • Memory usage during the load test: up to 105MB
  • Memory usage after the load test: 35-50MB

Rust:

  • Initial memory usage: 3-5MB
  • Memory usage during the load test: 35-45MB
  • Memory usage after the load test: remained at 35-45MB
  • Under extreme load (10k concurrent requests), memory usage increased to 319MB

Timing

NodeJS:

Request TypeTime
Single After Startup30-90ms
10k (500 Concurrent) AVG427ms
10k (1000 Concurrent) AVG685ms
100k (10k Concurrent) AVGNA

Rust:

Request TypeTime
Single After Startup10-65ms
10k (500 Concurrent) AVG162ms
10k (1000 Concurrent) AVG313ms
100k (10k Concurrent) AVG4837ms

Analysis and Conclusion

The initial single request timings are primarily influenced by database query times, which are not critical for this comparison. However, the performance under concurrent load reveals that Rust consistently outperforms NodeJS in all scenarios.

NodeJS, despite its reputation for handling concurrent requests efficiently, struggled with higher concurrency levels, leading to errors and higher average response times. In contrast, Rust’s asynchronous capabilities allowed it to handle the same load more gracefully, with lower response times. However, Rust’s memory usage was less stable, with significant increases under heavy load and slower release back to the operating system.

This experiment indicates that while NodeJS’s non-blocking I/O was revolutionary, modern asynchronous programming in languages like Rust can surpass its performance, particularly in high-concurrency environments. Thus, the relevance of NodeJS’s non-blocking I/O advantage appears diminished as other languages catch up and, in some cases, exceed its capabilities.

article
backend
experiments
javascript
nodejs
programming
rust