Skip to main content

Best Node.js Logging Libraries Compared in 2026

·PkgPulse Team

TL;DR

Pino for production Node.js services. Winston for flexible multi-destination logging. Morgan for Express HTTP request logging. The logging landscape has converged around structured JSON logging to stdout, with Pino as the performance leader. Choose based on whether you need raw speed, flexibility, or simplicity.

Key Takeaways

  • Winston: ~15M weekly downloads — largest install base
  • Pino: ~9M downloads — fastest, used by Fastify
  • Morgan: ~4M downloads — HTTP request logging for Express
  • Bunyan: ~2M downloads — original JSON logger, mostly legacy
  • Structured JSON to stdout is the production standard in 2026

The Libraries

import pino from 'pino';
const logger = pino({ level: 'info' });

logger.info({ requestId: '123', userId: 'u_456' }, 'Request processed');
// Output: {"level":30,"time":1709900000000,"requestId":"123","userId":"u_456","msg":"Request processed"}

// Child logger for request scoping
const reqLogger = logger.child({ requestId: req.id });
reqLogger.info('Processing');
reqLogger.error({ err }, 'Failed');

5-8x faster than Winston. Used by Fastify. Best for high-throughput APIs.

Winston — Most Flexible

const winston = require('winston');
const logger = winston.createLogger({
  transports: [
    new winston.transports.Console({ format: winston.format.json() }),
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    // Community: winston-cloudwatch, winston-mongodb, winston-datadog
  ],
});

logger.info('Server started', { port: 3000 });

Best for: Multiple log destinations, custom transports, complex formatting pipelines.

Morgan — HTTP Request Logging

const morgan = require('morgan');
app.use(morgan('combined')); // Apache combined log format
// Or: 'tiny', 'dev', custom format
// → GET /api/users 200 45ms - 1.2kb

// JSON format for production:
app.use(morgan((tokens, req, res) => JSON.stringify({
  method: tokens.method(req, res),
  url: tokens.url(req, res),
  status: parseInt(tokens.status(req, res)),
  responseTime: parseFloat(tokens['response-time'](req, res)),
})));

Best for: Express HTTP request logging. Use alongside Pino or Winston for application logs.

Bunyan — Legacy but Stable

const bunyan = require('bunyan');
const logger = bunyan.createLogger({ name: 'myapp' });
// JSON output, similar to Pino but slower and less maintained

Legacy choice. Still works, but Pino does everything Bunyan does, faster.


Performance Benchmark

100,000 log calls benchmark:

Library    | Time    | Relative
-----------|---------|----------
Pino       | 450ms   | 1x (baseline)
Bunyan     | 1,100ms | 2.4x
Winston    | 2,800ms | 6.2x

Why Pino is faster:
✓ Async stdout writes
✓ Minimal serialization
✓ No synchronous I/O
✓ log-stream processing outside Node.js event loop

Production Logging Architecture

Node.js app (Pino/Winston)
  ↓ JSON to stdout
Log collector (Fluentd, Vector, Filebeat)
  ↓ Forward + enrich
Log aggregator (Elasticsearch, Loki, CloudWatch)
  ↓ Index and store
Dashboard (Kibana, Grafana, CloudWatch Logs)
  ↓ Query and alert

All modern loggers output JSON to stdout — the collection and storage layer is separate.


Recommendations by Use Case

ScenarioRecommended Library
High-throughput API (Fastify/Hono)Pino (built-in)
Express REST APIPino + pino-http
HTTP request loggingMorgan
Multiple destinations neededWinston
Legacy codebaseKeep what you have
New project (any)Pino

Compare all logging library health scores on PkgPulse.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.