cheat sheet

winston

Package-level reference for winston on npm — transports, formats, levels, daily rotation, and production patterns.

winston

What it is

winston is a multi-transport logger for Node that prioritizes format flexibility over throughput. A single logger can simultaneously write to the console with colors, to a rotating file with JSON, to an HTTP endpoint with custom serialization, and to a third-party SaaS sink — each transport with its own level, format, and filter. The 3.x line introduced a composable format chain (format.combine(format.timestamp(), format.json(), …)) that replaced the older string-template approach.

Reach for winston when you need different sinks with different formats from one logger, when you log heavily to the console for human eyes, or when your team has years of winston configuration to preserve. Choose pino when log throughput matters more than format richness.

Install

bash
npm install winston

Output: added winston to dependencies

bash
pnpm add winston

Output: added 1 package, linked from store

bash
yarn add winston

Output: added winston

bash
bun add winston

Output: installed winston

For daily file rotation, add winston-daily-rotate-file:

bash
npm install winston-daily-rotate-file

Output: added winston-daily-rotate-file

Versioning & Node support

Current line is winston@3.x — extremely stable. The 3.x line has been the current version for years.

  • winston@3 — Node 12+ (effective floor — older Node still works but isn't tested). Composable formats, multiple transports, child loggers.
  • winston@2 — Historical (2017 and earlier). Legacy winston.add(transports.Console) API. Avoid for new projects.

Major releases are slow (3.x has been current since 2018). Pin minor in production ("winston": "3.x") and read the CHANGELOG before bumps.

Package metadata

  • Maintainer: Charlie Robbins + Winston contributors (winstonjs/winston)
  • Project home: github.com/winstonjs/winston
  • Docs: github.com/winstonjs/winston#readme
  • npm: npmjs.com/package/winston
  • License: MIT
  • First released: 2010
  • Downloads: ~13 million+ weekly downloads — among the most-downloaded logging packages.

Peer dependencies & extras

No peer-deps. Companion packages:

  • winston-daily-rotate-file — date-based file rotation
  • winston-transport — base class for custom transports
  • winston-elasticsearch — Elasticsearch transport
  • winston-loki — Grafana Loki transport
  • winston-mongodb — Mongo collection transport
  • winston-cloudwatch — AWS CloudWatch
  • @datadog/winston — Datadog HTTP transport
  • morgan — Express request logger (often used alongside winston)
  • express-winston — Express middleware emitting per-request logs

Alternatives

LoggerTrade-off
pino5-10× faster on hot paths; JSON-first; worker transports. Less format flexibility.
bunyanJSON-only; predecessor of pino. Maintenance has slowed.
log4jsLog4j-style hierarchical loggers and appenders. Less popular today.
signale / consolaPretty CLIs; not full production loggers.
roarrLightweight structured logger. Niche.
OpenTelemetry LogsNewer API; pairs with metrics + traces. Wrap winston with an OTel transport.

Real-world recipes

Basic logger

createLogger returns a logger. Add at least one transport — without transports, nothing is written.

javascript
import winston from "winston";

const log = winston.createLogger({
  level: "info",
  format: winston.format.json(),
  transports: [new winston.transports.Console()],
});

log.info("server starting");
log.warn({ port: 3000 }, "binding port");
log.error("boom", { code: "E_BOOM" });

Output: JSON to stdout — {"message":"server starting","level":"info"} per line. Pass an object as the second arg to add metadata.

Levels (default npm-style): error 0, warn 1, info 2, http 3, verbose 4, debug 5, silly 6 — lower numbers are more severe. level: "info" allows error/warn/info and drops the rest.

Multiple transports

The signature appeal of winston: one logger, multiple sinks, each with their own format and level.

javascript
import winston from "winston";

const log = winston.createLogger({
  level: "info",
  transports: [
    new winston.transports.Console({
      level: "debug",
      format: winston.format.combine(
        winston.format.colorize(),
        winston.format.timestamp(),
        winston.format.printf(({ timestamp, level, message }) =>
          `${timestamp} [${level}] ${message}`
        )
      ),
    }),
    new winston.transports.File({
      filename: "/var/log/app/app.log",
      level: "info",
      format: winston.format.combine(
        winston.format.timestamp(),
        winston.format.json()
      ),
    }),
    new winston.transports.File({
      filename: "/var/log/app/error.log",
      level: "error",
      format: winston.format.json(),
    }),
  ],
});

log.debug("verbose");  // console only
log.info("ready");     // console + app.log
log.error("boom");     // all three

Output: colored timestamped line in the console; JSON line in app.log; same JSON line in error.log for errors.

Custom log levels

Override the default npm levels with your own scheme — useful for matching backend conventions.

javascript
import winston from "winston";

const log = winston.createLogger({
  levels: { fatal: 0, error: 1, warn: 2, info: 3, debug: 4 },
  level: "info",
  format: winston.format.json(),
  transports: [new winston.transports.Console()],
});

winston.addColors({ fatal: "red", error: "red", warn: "yellow", info: "green", debug: "blue" });

log.fatal("unrecoverable");
log.info("startup complete");

Output: custom level names appear in JSON and (with colorize) in console output.

Format chain

Formats compose left-to-right. Each format receives info (the log entry object) and returns a transformed info.

javascript
import winston from "winston";

const log = winston.createLogger({
  format: winston.format.combine(
    winston.format.errors({ stack: true }),       // turn Error → { message, stack }
    winston.format.timestamp(),                    // add ISO timestamp
    winston.format.metadata({ key: "meta" }),     // group extra fields under `meta`
    winston.format.json()                          // serialize to JSON
  ),
  transports: [new winston.transports.Console()],
});

log.error(new Error("boom"));
log.info("hello", { reqId: "r-1" });

Output: Error logs include stack; info logs nest extras under meta: {"level":"info","message":"hello","timestamp":"…","meta":{"reqId":"r-1"}}.

Daily rotating file

winston-daily-rotate-file rotates by date pattern and optionally by size. Files older than the retention window are pruned automatically.

javascript
import winston from "winston";
import DailyRotateFile from "winston-daily-rotate-file";

const log = winston.createLogger({
  level: "info",
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json()
  ),
  transports: [
    new DailyRotateFile({
      filename: "/var/log/app/app-%DATE%.log",
      datePattern: "YYYY-MM-DD",
      maxSize: "20m",
      maxFiles: "14d",
      zippedArchive: true,
    }),
  ],
});

log.info({ uptime: process.uptime() }, "heartbeat");

Output: app-2026-05-31.log written today; yesterday's archived to app-2026-05-30.log.gz; files older than 14 days deleted.

Production deployment

Stdout vs files

In containers, prefer console / stdout — the platform captures it. Files inside containers require sidecars or volume mounts to be useful.

For long-running VM-based deploys, files + daily rotation + an external log shipper (Filebeat, Fluent Bit) is the classic pattern.

Async I/O caveats

winston transports write asynchronously. Use log.end() (or log.on("finish", cb)) before exit to flush.

javascript
log.on("finish", () => process.exit(0));
log.end();

Output: waits for buffered writes to drain.

For crash logs, the uncaughtException and unhandledRejection handlers must be sync — write a synchronous fallback line.

Exception + rejection handlers

winston has built-in support:

javascript
import winston from "winston";

const log = winston.createLogger({
  transports: [new winston.transports.Console()],
  exceptionHandlers: [new winston.transports.File({ filename: "exceptions.log" })],
  rejectionHandlers: [new winston.transports.File({ filename: "rejections.log" })],
  exitOnError: true,
});

Output: uncaught exceptions and unhandled rejections route to dedicated files; process exits after writing (set exitOnError: false to keep running).

Child loggers

For per-request context, use log.child({ reqId, userId }):

javascript
import winston from "winston";

const root = winston.createLogger({ transports: [new winston.transports.Console()] });

function handleRequest(reqId) {
  const log = root.child({ reqId });
  log.info("request started");
  // …
}

Output: every line from the child includes reqId automatically.

Performance tuning

winston is slower than pino — usually by 5-10× on the hot path. Most apps log a few times per request and never notice. If logging dominates your CPU profile, the levers are:

  • Reduce level. info in production, not debug. silly and verbose exist mostly for development.
  • silent: true on a transport disables it without removing it — useful for feature-flagged logging.
  • Avoid heavy format.printf callbacks. They run synchronously per log line.
  • Async transports. File and HTTP transports buffer internally. Console is synchronous on Linux when stdout is a TTY but pipes asynchronously.
  • Pre-resolve metadata. Computing process.memoryUsage() per log line is wasteful — cache and refresh on an interval.
  • Switch to pino on hot paths. A common pattern: keep winston for console + file in low-volume parts of the app; use pino for request logs.

Version migration guide

winston@3 is the current line

It has been stable for years. Most "migration" work is from winston@2winston@3, which is a near-rewrite.

From winston@2 to winston@3

  1. API entirely changed: winston.createLogger(opts) instead of new winston.Logger(opts).
  2. Transports moved from winston.transports.Console(opts) instantiated to passed as new winston.transports.Console(opts).
  3. Formats replaced the older template-string approach. format.combine(format.timestamp(), format.json()) is the canonical chain.
  4. Default levels stay the same (error, warn, info, ...) but custom levels move to levels: { ... } option.
  5. .log(level, message) and .<level>(message) both work; the latter is idiomatic.
  6. Color is opt-in via format.colorize() instead of an option.
  7. Profiling (log.profile) still works, but the output shape changed.

Migration is mechanical — npm install winston@3, search/replace, run tests. Expect a day for a medium codebase.

Within winston@3

Minor versions add transports, formats, and types. Breaking changes are rare. Read CHANGELOG before bumping.

Security considerations

  • Don't log full request bodies. Form posts, auth tokens, payment info. Mask with a custom format:

    javascript
    const redact = winston.format((info) => {
      if (info.password) info.password = "[REDACTED]";
      return info;
    })();
    

    Then format.combine(redact, format.json()).

  • Sanitize newlines in user input. log.info(user said ${input}) allows log injection. Always log as a field: log.info("user said", { input }).

  • Audit transports. Third-party transports (winston-elasticsearch, custom HTTP transports) inherit your TLS config. Verify the wire is encrypted.

  • File permissions. Logs often contain PII. Restrict file mode (0640), and consider running the log shipper as a separate user.

  • Don't log inside catch blocks of crypto operations. Stack traces can leak partial key material.

  • Bounded maxFiles / maxSize on rotating transports — without bounds, a misconfigured logger fills the disk.

  • uncaughtException handler is a last resort. It runs after the process is in an unknown state; flush minimal info and exit, not continue.

Testing & CI integration

For tests, mute or capture transports.

Mute:

javascript
import winston from "winston";

const log = winston.createLogger({
  transports: [new winston.transports.Console({ silent: process.env.NODE_ENV === "test" })],
});

Output: no console noise during tests.

Capture for assertion:

javascript
import { test, expect } from "vitest";
import winston from "winston";
import { Writable } from "node:stream";

test("logs error", () => {
  const lines = [];
  const stream = new Writable({
    write(chunk, _enc, cb) { lines.push(chunk.toString()); cb(); },
  });
  const log = winston.createLogger({
    format: winston.format.json(),
    transports: [new winston.transports.Stream({ stream })],
  });

  log.error("boom", { code: "E_BOOM" });
  log.end();

  const entry = JSON.parse(lines[0]);
  expect(entry.message).toBe("boom");
  expect(entry.code).toBe("E_BOOM");
});

Output: assertions on structured fields.

For Express integration tests, express-winston + supertest captures per-request logs.

Ecosystem integrations

ToolRole
express-winstonExpress middleware logging requests and errors.
morganApache-style HTTP request log; often piped through winston.
winston-daily-rotate-fileDate / size rotation.
winston-elasticsearchElasticsearch transport.
winston-lokiGrafana Loki transport.
winston-mongodbMongo collection transport.
winston-cloudwatchAWS CloudWatch.
@datadog/winstonDatadog HTTP intake.
winston-transportBase class for custom transports.
nest-winstonNestJS module.
OpenTelemetry winston instrumentationMap winston logs to OTel log records.

Troubleshooting common errors

Nothing is logged — no transports configured. createLogger({ transports: [] }) is silent.

Error: write EBADF — file transport's directory doesn't exist. winston-daily-rotate-file's createSymlink: true can fail if perms are wrong.

Logs interleaved between processes — multiple processes writing to the same file. Use rotation + per-process filenames, or write to stdout and let the platform handle it.

Logs missing after crash — buffered transport not flushed. log.end() and wait for finish event.

MaxListenersExceededWarning — creating loggers per-request rather than once. Create once, share, and use child loggers for context.

Color escape codes in production log filesformat.colorize() left in the file format. Apply colorize only to the console transport's format chain.

Console output unstructured — using printf format on a transport you expected to be JSON. Verify each transport's format.

Daily rotation not happeningdatePattern doesn't include enough granularity, or process restarts before rotation triggers. Verify pattern; let the transport run for a full day in dev.

High CPU when logging — synchronous format.printf running for every line at silly level. Lower the level or simplify the formatter.

When NOT to use this

  • Throughput-critical services. Pino beats winston by 5-10× on hot paths.
  • Edge runtimes. winston needs Node's fs/net. Cloudflare Workers / Vercel Edge use console.log + platform log capture.
  • You want a tree of hierarchical named loggers. log4js is closer to that model.
  • Very small services. console.log is fine for a single-purpose script; winston adds setup.
  • Distributed tracing first. OpenTelemetry logs (paired with metrics + traces) may be the better starting point for new platforms.
  • You already use pino. Don't mix loggers in one app; the output streams interleave inconsistently.

See also