Best Node.js Logging Libraries Compared in 2026
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
Pino — Fastest (Recommended for Performance)
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
| Scenario | Recommended Library |
|---|---|
| High-throughput API (Fastify/Hono) | Pino (built-in) |
| Express REST API | Pino + pino-http |
| HTTP request logging | Morgan |
| Multiple destinations needed | Winston |
| Legacy codebase | Keep what you have |
| New project (any) | Pino |
Compare all logging library health scores on PkgPulse.
See the live comparison
View pino vs. winston on PkgPulse →