This project aims to evaluate the performance of Node.js in event-driven microservices architecture under high-throughput workloads by replicating a realistic message processing pipeline.
To run and gather consistent performance metrics, almost all the code has been dockerized with specific configurations and locked down versions of dependencies. For example, every Node.js consumer will be resource-limited with 1.5 CPU cores and 1 GB RAM.
The project has been tested on Linux x64 and macOS arm platforms.
A machine with at least 6 CPU cores and 16 GB of RAM is recommended to run the tests effectively.
- Linux x64 or macOS arm platform
- Docker Engine or Docker Desktop
- bash (or compatible shell)
- git (only for cloning the repository)
- just
- curl
Three different workload types have been defined:
| Type | Operations | Purpose |
|---|---|---|
| I/O | PostgreSQL INSERT + file system write per message | Tests non-blocking I/O strengths |
| CPU | JSON.parse of a large payload + recursive Fibonacci(25) | Tests event loop blocking |
| Mixed | JSON.parse --> validate --> enrich --> SHA-256 --> DB INSERT --> file write | Realistic pipeline |
Each workload type will be tested under two different load patterns generated by the k6 load generator:
- Baseline: steady load of 500 messages per second.
- Burst: see sub-section below.
| Time window | Target load | Phase |
|---|---|---|
| 0:00 - 3:00 | 500 msg/s (flat) | warm-up/steady state |
| 3:00 - 3:30 | 500 --> 5,000 (ramp) | first spike |
| 3:30 - 5:00 | 5,000 msg/s (flat) | sustained medium load |
| 5:00 - 5:30 | 5,000 --> 10,000 (ramp) | peak spike |
| 5:30 - 7:00 | 10,000 msg/s (flat) | sustained peak load |
| 7:00 - 7:30 | 10,000 --> 500 (ramp down) | recovery begins |
| 7:30 - 10:00 | 500 msg/s (flat) | drain period |
By mixing each load generator pattern with each workload type, a total of 6 unique scenarios to test are possible. Each single scenario will be executed with 1, 2, and 4 consumer replicas to evaluate the scaling behavior. Additionally, to ensure statistical significance, each scenario will be repeated 5 times. This results in a total of 90 runs (6 scenarios X 3 replica counts X 5 repetitions).
| ID | Scenario | Load Pattern | Duration |
|---|---|---|---|
| S1 | Baseline I/O | Steady 500 msg/s | 10 min |
| S2 | Burst I/O | 500 --> 5,000 --> 10,000 msg/s | 10 min |
| S3 | Baseline CPU | Steady 500 msg/s | 10 min |
| S4 | Burst CPU | 500 --> 5,000 --> 10,000 msg/s | 10 min |
| S5 | Baseline Mixed | Steady 500 msg/s | 10 min |
| S6 | Burst Mixed | 500 --> 5,000 --> 10,000 msg/s | 10 min |
It's important to note that each test will last 10 minutes, it does not matter if the consumers actually finished processing all the messages or not. For each run a dedicated track record will be stored under results/<scenario>/<scenario>-R<replicas>-<repetition> (e.g. results/S2/S2-R4-1 for scenario S2 with 4 replicas on repetition 1).
These are the main services used across the project, all defined in the docker-compose.yml file:
- Kafka (message broker with KRaft mode)
- PostgreSQL (I/O target)
- Prometheus (metrics collection)
- Grafana (dashboards for live monitoring)
- cAdvisor (container-level CPU/memory metrics)
- Consumer (Node.js application being benchmarked)
- k6 (load generator)
- Python & Node (analysis and dev utilities)
This is a broad overview of the architecture used in the project:
Grafana k6 (xk6-kafka load generator)
│ produces messages
▼
Apache Kafka (KRaft mode, 8-partition topic)
│ consumes
▼
Consumer Group (1, 2, or 4 Node.js replicas)
│
│ one of three workload profiles per run:
├── I/O : PostgreSQL INSERT + file write/log
├── CPU : JSON.parse large payload + recursive Fibonacci
└── Mixed : JSON.parse --> PostgreSQL INSERT --> file write (realistic pipeline)
Metrics ──► Prometheus ──► Grafana
▲
└── cAdvisor (container CPU/memory)
Results ──► scripts/analyze.py (statistical summaries)
──► scripts/generate_graphs.py (graphs)
Run just with no arguments to list every available command grouped by category and refer directly to the justfile for anything not covered below (infrastructure, single scenarios, smoke tests, monitoring, formatting, cleanup...).
These commands showcase how to run the full experiment matrix and analyze the results.
# Build all the Docker images.
just build-all
# Run the full experiment matrix: `6 scenarios X 3 replica counts X 5 repetitions`
# (metrics written to `results/SX/` for each scenario).
just run-all
# Verify all 90 runs produced complete exports.
just audit-data
# Briefly check the results of the 90 runs.
just analyze > results/analyze.txt
# Generate the statistical summaries from the collected results.
just analyze-csv > results/stats.csv
just analyze-json > results/stats.json
# Generate all graphs from the collected results (written to `results/graphs/`).
just generate-graphsIn the experiments/ folder are available all the finished executed runs with all the relative metrics, stats generated, graphs and logs. The results/ folder instead is used only for storing the current running test.