DEV Community

Alex Spinov
Alex Spinov

Posted on

Pino Has the Fastest Node.js Logger — Here's Why You Should Switch from Winston

Winston was the go-to Node.js logger for years. But Pino is 5x faster — and here is why performance-focused teams are switching.

Why Pino is Faster

Pino logs as NDJSON (newline-delimited JSON) with minimal overhead. No synchronous operations, no string formatting in the hot path. Just fast, structured JSON.

Winston: ~10,000 ops/sec
Pino:    ~52,000 ops/sec
Enter fullscreen mode Exit fullscreen mode

Quick Start

bun add pino pino-pretty
Enter fullscreen mode Exit fullscreen mode
import pino from "pino";

const logger = pino({ level: "info" });

logger.info("Server started");
logger.info({ port: 3000 }, "Listening on port");
logger.error({ err: new Error("DB failed") }, "Database connection error");
logger.warn({ userId: "123", action: "login_failed" }, "Authentication failed");
Enter fullscreen mode Exit fullscreen mode

Output:

{"level":30,"time":1711800000000,"msg":"Server started"}
{"level":30,"time":1711800000001,"port":3000,"msg":"Listening on port"}
Enter fullscreen mode Exit fullscreen mode

Pretty Printing (Development)

# Pipe through pino-pretty
node app.js | pino-pretty
Enter fullscreen mode Exit fullscreen mode
// Or configure in code (dev only!)
const logger = pino({
  transport: {
    target: "pino-pretty",
    options: { colorize: true, translateTime: "SYS:standard" }
  }
});
Enter fullscreen mode Exit fullscreen mode

Child Loggers

const logger = pino();

// Create child logger with bound context
const requestLogger = logger.child({ requestId: "abc-123" });
requestLogger.info("Processing request"); // includes requestId in every log

const userLogger = requestLogger.child({ userId: "user_456" });
userLogger.info("User action"); // includes both requestId AND userId
Enter fullscreen mode Exit fullscreen mode

With Express/Fastify

// Express
import pinoHttp from "pino-http";
app.use(pinoHttp());

// Fastify (built-in!)
const fastify = Fastify({ logger: true });
// That is it — Fastify uses Pino natively
Enter fullscreen mode Exit fullscreen mode

Log Redaction (Hide Sensitive Data)

const logger = pino({
  redact: {
    paths: ["password", "creditCard", "req.headers.authorization"],
    censor: "[REDACTED]"
  }
});

logger.info({ user: "alice", password: "secret123" }, "Login attempt");
// Output: {"user":"alice","password":"[REDACTED]","msg":"Login attempt"}
Enter fullscreen mode Exit fullscreen mode

Transport (Send Logs Anywhere)

const logger = pino({
  transport: {
    targets: [
      { target: "pino-pretty", level: "info", options: { destination: 1 } },
      { target: "pino/file", level: "error", options: { destination: "./errors.log" } },
      { target: "pino-elasticsearch", level: "info", options: { index: "app-logs" } }
    ]
  }
});
Enter fullscreen mode Exit fullscreen mode

Pino vs Winston vs Bunyan

Feature Pino Winston Bunyan
Speed 52K ops/s 10K ops/s 8K ops/s
Output NDJSON Flexible JSON
Child Loggers Yes Yes Yes
Redaction Built-in Plugin No
Pretty Print pino-pretty Built-in bunyan CLI
Transports Worker thread Sync Streams
Fastify Native Plugin Plugin

Building logging for scraping pipelines? Check out my web scraping actors on Apify Store — production-ready with structured logging. For custom solutions, email spinov001@gmail.com.

Top comments (0)