We already know that logging is affecting performance, especially when we use console.log
because its a syncronous process. In nodejs, we have to use asyncronous process to get the best performance. In this article, we will be comparing the performance difference between no logging, console logging, the most popular logging library winston
and the fastest logging library pino
.
Test Scenario
All the application needed for the test is installed on my notebook. This is my notebook & App spec:
- CPU: Intel Core i3-6006U, 2 cores, 4 threads.
- RAM: 16 GB
- OS: Ubuntu 18.04 WSL
- Node Version: 14.17.2
- Express Version: 4.17.2
- Fastify Version: 3.25.3
- ApacheBench Version: 2.3
- Pino Version: 7.6.4
- Winston Version: 3.4.0
The application we used to test is ab
(ApacheBench). the ab
will hit the nodejs server 10k
times with 1000
concurrency with this is the command:
ab -n 10000 -c 1000 -H "Authorization: Bearer {token}" http://localhost:3000/getproducts
The webserver will first verify the bearer token from the request, if the token verified it will return the list of products (20 rows) which it gets from database query. We use MySQL database in this test. You can see the test topology below:
ab ------> NodeJS ------> MySQL
In this test, We will compare:
- NodeJS using
fastify
framework with no logging. - NodeJS using
fastify
framework withpino
sync logging. - NodeJS using
fastify
framework withpino
async logging. - NodeJS using
fastify
framework withwinston
logging. - Nodejs using
fastify
framework withconsole
logging.
And we will compare 1 and 2 times logging in 1 request for each setup above:
- For 1 times logging, we will write log right after the token verified.
- For 2 times logging, we will write log right after the token verified and after database query.
Fastify with No Logging Code
This is the app.js
using no logging implementation:
const fastify = require("fastify");
const app = fastify();
app.register(require("fastify-mysql"), {
connectionLimit: 100,
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME,
promise: true,
});
app.register(require("fastify-jwt"), {
secret: process.env.JWT_SECRET,
});
app.get("/getproduct", async (req, res) => {
const decoded = await req.jwtVerify();
if (!decoded) {
res.status(403).send({ message: "token not valid" });
return;
}
const [results] = await app.mysql.query("SELECT * FROM product");
res.send({ results });
});
app.listen(3000);
As you can see this is plain source code without logging
Fastify with Pino (Sync Mode) Logging Code
This is the app.js
using pino
logging implementation with sync mode:
const fastify = require("fastify");
const pino = require("pino");
const app = fastify();
const logger = pino();
app.register(require("fastify-mysql"), {
connectionLimit: 100,
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME,
promise: true,
});
app.register(require("fastify-jwt"), {
secret: process.env.JWT_SECRET,
});
app.get("/getproduct", async (req, res) => {
const decoded = await req.jwtVerify();
if (!decoded) {
res.status(403).send({ message: "token not valid" });
return;
}
logger.info(decoded);
const [results] = await app.mysql.query("SELECT * FROM product");
logger.info(results); // Commented when testing 1 time logging
res.send({ results });
});
app.listen(3000);
In this source code, we are using pino
logging with default option, without activating async
mode.
Fastify with Pino (Async Mode) Logging Code
This is the app.js
using pino
logging implementation with async mode:
const fastify = require("fastify");
const pino = require("pino");
const app = fastify();
const logger = pino(pino.destination({ sync: false }));
app.register(require("fastify-mysql"), {
connectionLimit: 100,
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME,
promise: true,
});
app.register(require("fastify-jwt"), {
secret: process.env.JWT_SECRET,
});
app.get("/getproduct", async (req, res) => {
const decoded = await req.jwtVerify();
if (!decoded) {
res.status(403).send({ message: "token not valid" });
return;
}
logger.info(decoded);
const [results] = await app.mysql.query("SELECT * FROM product");
logger.info(results); // Commented when testing 1 time logging
res.send({ results });
});
app.listen(3000);
This time, we are using pino
logging with async mpde by declaring sync: false
.
Fastify with Winston Logging Code
This is the app.js
using winston
logging implementation:
const fastify = require("fastify");
const { createLogger, transports } = require("winston");
const app = fastify();
const logger = createLogger({
transports: [new transports.Console()],
});
app.register(require("fastify-mysql"), {
connectionLimit: 100,
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME,
promise: true,
});
app.register(require("fastify-jwt"), {
secret: process.env.JWT_SECRET,
});
app.get("/getproduct", async (req, res) => {
const decoded = await req.jwtVerify();
if (!decoded) {
res.status(403).send({ message: "token not valid" });
return;
}
logger.info(decoded);
const [results] = await app.mysql.query("SELECT * FROM product");
logger.info(results); // Commented when testing 1 time logging
res.send({ results });
});
app.listen(3000);
In this code, we are using winston
package with default option with console transport.
Fastify with Console Logging Code
This is the app.js
using console
logging implementation:
const fastify = require("fastify");
const app = fastify();
app.register(require("fastify-mysql"), {
connectionLimit: 100,
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME,
promise: true,
});
app.register(require("fastify-jwt"), {
secret: process.env.JWT_SECRET,
});
app.get("/getproduct", async (req, res) => {
console.log("incoming requset")
const decoded = await req.jwtVerify();
if (!decoded) {
res.status(403).send({ message: "token not valid" });
return;
}
console.log(decoded);
const [results] = await app.mysql.query("SELECT * FROM product");
console.log(results); // Commented when testing 1 time logging
res.send({ results });
});
app.listen(3000);
Last, we are not using any external package for logging. Just plain console.log
logging.
The Test Result
In this segment we will show you the result of the test (10k request, 1k concurrent). The test scenario is done 10 times each, we only display the average results here so you are not overwhelmed by the information:
Time Taken For Tests
Test | 1 times logging | 2 times logging |
---|---|---|
Console.log | 12.687 s | 50.627 s |
Winston | 11.44 s | 28.51 s |
Pino | 11.125 s | 27.627 s |
Pino Async | 7.702 s | 14.34 s |
No Logging | 6.149 s | 6.149 s |
Requests Per Second
Test | 1 times logging | 2 times logging |
---|---|---|
Console.log | 801.134 /s | 198.698 /s |
Winston | 886.157 /s | 351.017 /s |
Pino | 910.048 /s | 362.814 /s |
Pino Async | 1309.895 /s | 770.914 /s |
No Logging | 1648.524 /s | 1648.524 /s |
Conclusion
There are some key point when I was doing this experiment:
- When using
pino async
, the performance reduced around20%
when logging once, and53%
when logging twice. - When using
pino
, the performance reduced around45%
when logging once, and78%
when logging twice. - When using
winston
, the performance reduced around46%
when logging once, and79%
when logging twice. - When using
console
, the performance reduced around51%
when logging once, and88%
when logging twice. - We can see from the result above that the performance is reduced a lot even when we try to log only once. So be careful when you want to log something. Only log when you really need to especially in production environment.
- Do not use standard
console.log
to log in production environment. It is worst in term of performance when you compare it with other library such aspino
orwinston
. - If you really care about performance, then choose
pino
in async mode for logging, it is much better thanwinston
in term of performance as you can see from the test results. This is why fastify usepino
for its logging.
That’s it! Now you know how logging really affect performance, even when you are using the fastest one. So, use them wisely.