Pino
Pino is a logger built around one obsession: speed. It is several times faster than Winston because of two design choices — it emits JSON by default (no string formatting in the hot path) and it does the expensive work (pretty-printing, shipping to other services) in a separate worker thread or process, so your application thread is barely interrupted. For high-throughput Node services, Pino is the de-facto standard. This page covers the basics, child loggers, redaction, transports, the pino-http middleware, and the crucial "log fast, transport elsewhere" philosophy.
Setup and basics
npm install pino
logger.js
import pino from 'pino'
export const logger = pino({
level: process.env.LOG_LEVEL || 'info',
// Rename default keys to match your log platform's conventions:
formatters: {
level: (label) => ({ level: label }), // log level as text, not number
},
})
// Usage — message-last OR object-first, both work:
logger.info('server started')
logger.info({ port: 3000, pid: process.pid }, 'server started')
logger.error({ err }, 'payment failed'){"level":"info","time":1750497601004,"pid":4821,"port":3000,"msg":"server started"}Why Pino is fast — async transports
Pretty-printing in development
npm install --save-dev pino-pretty
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
// Use pino-pretty ONLY in development — keep raw JSON in production:
transport:
process.env.NODE_ENV !== 'production'
? { target: 'pino-pretty', options: { colorize: true, translateTime: 'HH:MM:ss' } }
: undefined,
})# With pino-pretty (development):
[09:25:02] INFO: server started
port: 3000
pid: 4821Redacting sensitive fields
const logger = pino({
redact: {
paths: [
'req.headers.authorization',
'req.headers.cookie',
'body.password',
'body.creditCard',
'*.ssn', // any ssn at any top-level object
],
censor: '[REDACTED]', // or remove: true to delete the key entirely
},
})
logger.info({ body: { email: 'a@x.com', password: 'hunter2' } }, 'login'){"level":"info","time":...,"body":{"email":"a@x.com","password":"[REDACTED]"},"msg":"login"}HTTP request logging with pino-http
npm install pino-http
import pinoHttp from 'pino-http'
import { logger } from './logger.js'
const app = express()
app.use(pinoHttp({ logger }))
app.get('/orders/:id', (req, res) => {
// req.log is a child logger with a per-request id already attached:
req.log.info({ orderId: req.params.id }, 'fetching order')
res.json({ id: req.params.id })
}){"level":"info","time":...,"req":{"id":1,"method":"GET","url":"/orders/9981"},
"res":{"statusCode":200},"responseTime":12,"msg":"request completed"}Child loggers
// Bind context that applies to a group of related logs:
const orderLog = logger.child({ module: 'orders', orderId: 9981 })
orderLog.info('processing started') // includes module + orderId
orderLog.error({ err }, 'processing failed')Pino vs Winston — choosing
Consideration | Pino | Winston |
|---|---|---|
Performance | Fastest — async, JSON-first | Slower — inline formatting |
Default output | Structured JSON | Configurable (often needs setup) |
Transports | Worker-thread based, fewer built-in | Many built-in, very flexible routing |
Ecosystem |
| Huge transport ecosystem |
Best for | High-throughput APIs, performance-sensitive | Complex multi-destination routing |