mrkeyoor.com_
Thu 06 Aug 07:41 UTC
npmInfraupdated 06 Aug 2026

winston

winston is the general-purpose logger for Node.js. You build a logger with winston.createLogger(), hand it a format and a list of transports (Console, File, Http, or one of the many community transports), and every logger.info() call fans out to all of them. Each transport can carry its own level and its own format, so error lines land in a file while everything goes to stdout. It has been the default answer for Node logging for well over a decade, and the 3.x line has been current since 2018.

Verdict

Pick winston when you want log routing, formatting, and rotation configured in one place inside your app, and you can spend the CPU that costs. Pick pino when logs are JSON going to a collector and latency is the thing you are protecting.

API stability5/5createLogger, formats, and transports have not changed shape since 3.0 landed in June 2018, and 3.19.0 is additive; the only real trap is 2.x-era tutorials whose API does not work on 3.x.
Docs3/5One very long README plus docs/transports.md covers most of it, but there is no docs site, and the format catalog you actually need lives in a separate repo (logform).
Maintenance3/5Pushed July 2026 with 3.19.0 out in December 2025, so releases still land, but there are 459 open issues (527 counting PRs) against 24.5k stars and no roadmap beyond bug fixes.
Ecosystem5/5winston-transport gives community transports a stable base class, winston-daily-rotate-file and express-winston cover the common gaps, and most log vendors ship a winston transport.

Use it if

  • You want per-destination routing: errors to one file, everything to stdout, warnings to an HTTP endpoint, each with a separate format and level
  • You need log file rotation handled inside the process via winston-daily-rotate-file instead of logrotate or a sidecar container
  • You want uncaught exceptions and unhandled promise rejections captured into a dedicated destination with exceptionHandlers and rejectionHandlers
  • You are inheriting a Node codebase that already logs through winston, possibly with custom transports written against winston-transport
Skip it if

Setup reality

npm install winston gives you a working logger in five lines, but the default logger exported by require('winston') has no transports at all, and the README warns that leaving it that way can push memory usage up. Real setups mean composing formats with format.combine in the correct order (colorize has to come before whatever renders the text you want colored), installing winston-daily-rotate-file separately if you want rotation, and knowing that printf-style %s interpolation is silently inert unless you enable format.splat(). TypeScript declarations ship in the package. Flushing before process exit is manual: call logger.end() and wait for the finish event.

Patterns

Create a logger with file and console transportscreate-logger

const winston = require('winston');

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  defaultMeta: { service: 'user-service' },
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' }),
  ],
});

logger.info('server started', { port: 3000 });

Build your own logger rather than using require('winston') directly: the default logger has zero transports, and the README warns that leaving it empty can raise memory usage.

Add a readable console transport outside productionconsole-in-dev

if (process.env.NODE_ENV !== 'production') {
  logger.add(new winston.transports.Console({
    format: winston.format.combine(
      winston.format.colorize(),
      winston.format.simple(),
    ),
  }));
}

colorize() has to come before the formatter that renders the text, otherwise there is nothing colored yet when it runs.

Write your own line formatcustom-printf-format

const { createLogger, format, transports } = require('winston');
const { combine, timestamp, label, printf } = format;

const line = printf(({ level, message, label, timestamp }) =>
  `${timestamp} [${label}] ${level}: ${message}`);

const logger = createLogger({
  format: combine(label({ label: 'api' }), timestamp(), line),
  transports: [new transports.Console()],
});

printf must be last in the combine chain; anything after it operates on an info object whose rendered message is already fixed.

Attach request context to every linechild-logger

app.use((req, res, next) => {
  req.log = logger.child({ requestId: req.id });
  next();
});

// later
req.log.warn('slow query', { ms: 812 });

Child metadata merges into every call. The README flags .child() as unreliable if you have subclassed Logger yourself, so avoid combining the two.

Use printf-style placeholdersstring-interpolation

const logger = winston.createLogger({
  format: winston.format.combine(
    winston.format.splat(),
    winston.format.simple(),
  ),
  transports: [new winston.transports.Console()],
});

logger.info('imported %d rows from %s', 4210, 'orders.csv');

Without format.splat() the placeholders are printed literally; this catches people who copy console.log habits into winston.

Define your own levels and colorscustom-levels

const levels = { fatal: 0, error: 1, warn: 2, info: 3, trace: 4 };
const colors = { fatal: 'bold red', error: 'red', warn: 'yellow', info: 'green', trace: 'grey' };

winston.addColors(colors);
const logger = winston.createLogger({ levels, level: 'info' });
logger.fatal('out of disk');

Lower number means higher severity (RFC5424 order), and levels can only be set when the logger is created, not swapped later.

Capture uncaught exceptions and rejectionshandle-exceptions

const logger = winston.createLogger({
  transports: [new winston.transports.File({ filename: 'combined.log' })],
  exceptionHandlers: [new winston.transports.File({ filename: 'exceptions.log' })],
  rejectionHandlers: [new winston.transports.File({ filename: 'rejections.log' })],
  exitOnError: false,
});

winston exits the process after logging an uncaught exception unless exitOnError is false; exitOnError can also be a function so you keep running for specific error codes.

Rotate log files by datedaily-rotate-file

require('winston-daily-rotate-file');

const rotate = new winston.transports.DailyRotateFile({
  filename: 'app-%DATE%.log',
  datePattern: 'YYYY-MM-DD',
  zippedArchive: true,
  maxSize: '20m',
  maxFiles: '14d',
});

logger.add(rotate);

Separate npm install; requiring the package registers DailyRotateFile onto winston.transports as a side effect.

Drop log records that match a conditionfilter-logs

const dropHealthChecks = winston.format((info) => {
  if (info.path === '/healthz') return false;
  return info;
});

const logger = winston.createLogger({
  format: winston.format.combine(dropHealthChecks(), winston.format.json()),
  transports: [new winston.transports.Console()],
});

Returning a falsy value discards the record and stops the rest of the combine chain, which is also how you build redaction since winston has no redact option.

Give each transport its own level and formatper-transport-config

const logger = winston.createLogger({
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error', format: winston.format.json() }),
    new winston.transports.Console({ level: 'debug', format: winston.format.simple() }),
  ],
});

// change one at runtime
logger.transports[1].level = 'warn';

A transport level is a maximum verbosity for that destination and is independent of the logger-wide level, so the stricter of the two wins.

Wait for logs to be written before exitingflush-before-exit

logger.on('finish', () => process.exit(0));
logger.error('fatal startup failure');
logger.end();

Calling process.exit() straight after logger.error() can truncate File and Http transports; the finish event is the only signal that every transport drained.

Time a block of workprofile-timing

const profiler = logger.startTimer();
await rebuildSearchIndex();
profiler.done({ message: 'index rebuilt' });

// or the named form
logger.profile('import');
await importRows();
logger.profile('import');

Profile output is logged at info by default; pass { level: 'debug' } in the metadata object to move it down.

Alternatives

PackageRegistryPick it when
pinonpmYou log JSON to a collector and want the lowest possible overhead in your process.
consolanpmYou are writing a CLI and want pretty human-readable output rather than transport routing.
loglevelnpmYou want a tiny leveled logger that works the same in the browser and in Node with no transport layer at all.