Pino logs are so fast because they don’t actually do much when they write a log line.

Let’s see Pino in action. Imagine we have a simple Express.js app:

const express = require('express');
const pino = require('pino');

const logger = pino({
  level: 'info',
  prettyPrint: process.env.NODE_ENV !== 'production'
});

const app = express();

app.get('/', (req, res) => {
  logger.info('Received request for /');
  res.send('Hello World!');
});

app.get('/error', (req, res) => {
  logger.error('Simulating an error');
  res.status(500).send('Something broke!');
});

const port = 3000;
app.listen(port, () => {
  logger.info(`Server listening on port ${port}`);
});

When you run this (e.g., NODE_ENV=development node app.js), and then hit http://localhost:3000 in your browser, you’ll see output like this in your console:

{
  "pid": 12345,
  "hostname": "my-laptop",
  "level": 30,
  "msg": "Server listening on port 3000",
  "time": "2023-10-27T10:00:00.000Z"
}

And if you hit /error:

{
  "pid": 12345,
  "hostname": "my-laptop",
  "level": 50,
  "msg": "Simulating an error",
  "time": "2023-10-27T10:01:00.000Z"
}

Notice how each log line is a self-contained JSON object. This is the core of structured logging. Instead of just dumping a string, we’re emitting a machine-readable record with fields like pid, hostname, level, msg, and time.

Pino’s magic lies in its extreme optimization for this JSON output. It bypasses many of the overheads associated with traditional logging libraries. It uses highly optimized C++ bindings (via sonic-boom) for writing to stdout or files, and its JavaScript core is meticulously crafted to minimize garbage collection and CPU cycles during the logging process. This means you can log a lot without impacting your application’s performance.

The real power comes from how you use these structured logs. In production, you’d typically disable prettyPrint and pipe stdout to a log aggregation system like Elasticsearch, Splunk, or Datadog. These systems can then parse the JSON, index the fields, and allow you to search, filter, and visualize your logs based on any of the fields.

For example, you can add context to your logs:

const logger = pino({
  level: 'info',
  base: null // Remove default pid and hostname for custom context
});

app.get('/user/:id', (req, res) => {
  const userId = req.params.id;
  logger.info({ userId: userId, path: req.path }, 'User requested profile');
  res.send(`Profile for user ${userId}`);
});

Now, a log line for this route might look like:

{
  "userId": "123",
  "path": "/user/123",
  "level": 30,
  "msg": "User requested profile",
  "time": "2023-10-27T10:05:00.000Z"
}

This allows you to easily query for all requests made by a specific userId or all requests hitting a particular path.

The level field is not just a string; it’s a number. info is 30, warn is 40, error is 50, fatal is 60. This mapping allows for efficient filtering at the system level. You can tell your log aggregator to only ingest logs with a level of 50 or higher, for instance.

When you configure Pino, you’re essentially telling it how to transform your log messages into JSON. The level option determines the minimum severity to log. base is an object containing default fields that are added to every log record; setting it to null or an empty object ({}) removes the default pid and hostname, which is useful if your log aggregation system adds these automatically or if you want to provide your own custom base fields. The transport option is where you’d configure writing to files or sending logs elsewhere, often using sonic-boom for maximum performance.

The serializers option is a powerful way to automatically transform complex objects into a log-friendly format. For example, you can serialize an Error object to include its stack trace:

const logger = pino({
  level: 'error',
  serializers: {
    err: pino.stdSerializers.err
  }
});

try {
  throw new Error('Something went wrong!');
} catch (e) {
  logger.error({ err: e }, 'An error occurred');
}

This will produce a log line including the full error stack.

The most common mistake when adopting Pino for production is forgetting to configure a proper log shipping mechanism. If you just run node app.js in production, your logs are going to stdout on that specific server, which is usually not what you want. You’ll need to set up a service like Filebeat, Fluentd, or Logstash to collect logs from stdout (or a file if you configure Pino to write to one) and send them to your central logging store.

The next step after mastering structured logging with Pino is understanding how to correlate log events across different services.

Want structured learning?

Take the full Nodejs course →