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
npm install winston
Output: added winston to dependencies
pnpm add winston
Output: added 1 package, linked from store
yarn add winston
Output: added winston
bun add winston
Output: installed winston
For daily file rotation, add winston-daily-rotate-file:
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). Legacywinston.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 rotationwinston-transport— base class for custom transportswinston-elasticsearch— Elasticsearch transportwinston-loki— Grafana Loki transportwinston-mongodb— Mongo collection transportwinston-cloudwatch— AWS CloudWatch@datadog/winston— Datadog HTTP transportmorgan— Express request logger (often used alongside winston)express-winston— Express middleware emitting per-request logs
Alternatives
| Logger | Trade-off |
|---|---|
pino | 5-10× faster on hot paths; JSON-first; worker transports. Less format flexibility. |
bunyan | JSON-only; predecessor of pino. Maintenance has slowed. |
log4js | Log4j-style hierarchical loggers and appenders. Less popular today. |
signale / consola | Pretty CLIs; not full production loggers. |
roarr | Lightweight structured logger. Niche. |
| OpenTelemetry Logs | Newer 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.
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.
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.
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.
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.
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.
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:
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 }):
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.infoin production, notdebug.sillyandverboseexist mostly for development. silent: trueon a transport disables it without removing it — useful for feature-flagged logging.- Avoid heavy
format.printfcallbacks. 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@2 → winston@3, which is a near-rewrite.
From winston@2 to winston@3
- API entirely changed:
winston.createLogger(opts)instead ofnew winston.Logger(opts). - Transports moved from
winston.transports.Console(opts)instantiated to passed asnew winston.transports.Console(opts). - Formats replaced the older template-string approach.
format.combine(format.timestamp(), format.json())is the canonical chain. - Default levels stay the same (
error,warn,info, ...) but custom levels move tolevels: { ... }option. .log(level, message)and.<level>(message)both work; the latter is idiomatic.- Color is opt-in via
format.colorize()instead of an option. - 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:
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/maxSizeon rotating transports — without bounds, a misconfigured logger fills the disk. -
uncaughtExceptionhandler 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:
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:
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
| Tool | Role |
|---|---|
express-winston | Express middleware logging requests and errors. |
morgan | Apache-style HTTP request log; often piped through winston. |
winston-daily-rotate-file | Date / size rotation. |
winston-elasticsearch | Elasticsearch transport. |
winston-loki | Grafana Loki transport. |
winston-mongodb | Mongo collection transport. |
winston-cloudwatch | AWS CloudWatch. |
@datadog/winston | Datadog HTTP intake. |
winston-transport | Base class for custom transports. |
nest-winston | NestJS module. |
| OpenTelemetry winston instrumentation | Map 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 files — format.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 happening — datePattern 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 useconsole.log+ platform log capture. - You want a tree of hierarchical named loggers.
log4jsis closer to that model. - Very small services.
console.logis 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
- npm: pino — high-throughput JSON logger
- Concept: json — structured log shapes
- Concept: async — transport flush, backpressure