Rotate Logs Daily with Winston
How to configure daily log rotation in Node.js using winston and winston-daily-rotate-file, including size limits, retention, compression, and transport combining.
Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.
Overview
Winston is a versatile Node.js logger. The winston-daily-rotate-file transport writes logs to a file that rotates daily (or when it reaches a size limit). Old files are named with a date stamp and optionally compressed and deleted after a retention period. This prevents log files from growing indefinitely and consuming all disk space.
When to Use
- Applications that write logs to files (not just stdout) and need rotation
- Environments where a log collector (Fluent Bit, Filebeat) is not available
- Long-running services where unbounded log growth would fill the disk
- Compliance requirements that mandate keeping logs for N days then deleting them
When NOT to Use
- Containerized applications — write JSON to stdout and let the orchestrator handle rotation
- High-throughput logging — pino with stdout is faster than Winston with file transports
- Simple scripts —
console.logor a single log file is sufficient
Solution
Setup
npm install winston winston-daily-rotate-file
Basic daily rotation
const winston = require("winston");
const DailyRotateFile = require("winston-daily-rotate-file");
const logger = winston.createLogger({
level: "info",
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json(),
),
transports: [
new DailyRotateFile({
filename: "logs/application-%DATE%.log",
datePattern: "YYYY-MM-DD",
maxSize: "20m",
maxFiles: "14d",
}),
],
});
logger.info("server_started", { port: 3000 });
logger.warn("cache_miss", { key: "user:42" });
logger.error("database_error", { error: "Connection refused" });
This creates files like logs/application-2026-07-05.log, rotates daily, caps each file at 20MB, and keeps 14 days of logs.
Separate error log file
const logger = winston.createLogger({
level: "info",
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json(),
),
transports: [
new DailyRotateFile({
filename: "logs/application-%DATE%.log",
datePattern: "YYYY-MM-DD",
maxSize: "20m",
maxFiles: "14d",
level: "info",
}),
new DailyRotateFile({
filename: "logs/error-%DATE%.log",
datePattern: "YYYY-MM-DD",
maxSize: "20m",
maxFiles: "30d",
level: "error",
}),
],
});
logger.info("goes_to_application_log");
logger.error("goes_to_both_logs");
Adding console output in development
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || "info",
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json(),
),
transports: [
new DailyRotateFile({
filename: "logs/application-%DATE%.log",
datePattern: "YYYY-MM-DD",
maxSize: "20m",
maxFiles: "14d",
}),
],
});
if (process.env.NODE_ENV !== "production") {
logger.add(
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple(),
),
}),
);
}
Zipped archive rotation
const transport = new DailyRotateFile({
filename: "logs/application-%DATE%.log",
datePattern: "YYYY-MM-DD",
maxSize: "20m",
maxFiles: "30d",
zippedArchive: true,
auditFile: "logs/audit.json",
});
const logger = winston.createLogger({
transports: [transport],
});
Old log files are compressed with gzip. The audit file tracks which files exist and their sizes.
Custom log format
const logFormat = winston.format.printf(({ timestamp, level, message, ...meta }) => {
return JSON.stringify({
timestamp,
level,
message,
...meta,
});
});
const logger = winston.createLogger({
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
logFormat,
),
transports: [
new DailyRotateFile({
filename: "logs/application-%DATE%.log",
datePattern: "YYYY-MM-DD",
maxSize: "20m",
maxFiles: "14d",
}),
],
});
logger.error("something_failed", { error: new Error("boom") });
// Output: {"timestamp":"2026-07-05T10:30:00Z","level":"error","message":"something_failed","error":{"stack":"..."}}
Handling rotation events
const transport = new DailyRotateFile({
filename: "logs/application-%DATE%.log",
datePattern: "YYYY-MM-DD",
maxSize: "20m",
maxFiles: "14d",
});
transport.on("rotate", (oldFilename, newFilename) => {
console.log(`Log rotated from ${oldFilename} to ${newFilename}`);
});
transport.on("archive", (zipFilename) => {
console.log(`Log archived to ${zipFilename}`);
});
transport.on("logRemoved", (removedFilename) => {
console.log(`Old log removed: ${removedFilename}`);
});
const logger = winston.createLogger({ transports: [transport] });
Size-based rotation without date
const transport = new DailyRotateFile({
filename: "logs/application.log",
maxSize: "10m",
maxFiles: 5,
datePattern: "YYYY-MM-DD-HH",
});
const logger = winston.createLogger({ transports: [transport] });
This rotates when the file reaches 10MB, keeps 5 files, and uses hourly date patterns.
Integration with Express
const express = require("express");
const winston = require("winston");
const DailyRotateFile = require("winston-daily-rotate-file");
const morgan = require("morgan");
const logger = winston.createLogger({
level: "info",
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json(),
),
transports: [
new DailyRotateFile({
filename: "logs/access-%DATE%.log",
datePattern: "YYYY-MM-DD",
maxSize: "20m",
maxFiles: "14d",
}),
new DailyRotateFile({
filename: "logs/error-%DATE%.log",
datePattern: "YYYY-MM-DD",
maxSize: "20m",
maxFiles: "30d",
level: "error",
}),
],
});
const app = express();
const stream = { write: (msg) => logger.info(msg.trim()) };
app.use(morgan("combined", { stream }));
app.get("/api/users/:id", (req, res) => {
logger.info("user_fetched", { user_id: req.params.id });
res.json({ id: req.params.id, name: "Alice" });
});
app.listen(3000);
Variants
Using Winston with TypeScript
import winston from "winston";
import DailyRotateFile from "winston-daily-rotate-file";
const logger = winston.createLogger({
level: "info",
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json(),
),
transports: [
new DailyRotateFile({
filename: "logs/application-%DATE%.log",
datePattern: "YYYY-MM-DD",
maxSize: "20m",
maxFiles: "14d",
}),
],
});
interface LogMetadata {
user_id?: number;
request_id?: string;
duration_ms?: number;
}
function logInfo(message: string, meta: LogMetadata = {}): void {
logger.info(message, meta);
}
Combining with Sentry transport
const Sentry = require("@sentry/node");
const winston = require("winston");
const DailyRotateFile = require("winston-daily-rotate-file");
Sentry.init({ dsn: process.env.SENTRY_DSN });
const sentryTransport = new winston.transports.Stream({
stream: {
write: (message) => {
const logEntry = JSON.parse(message);
if (logEntry.level === "error") {
Sentry.captureException(new Error(logEntry.message));
}
},
},
});
const logger = winston.createLogger({
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json(),
),
transports: [
new DailyRotateFile({
filename: "logs/application-%DATE%.log",
datePattern: "YYYY-MM-DD",
maxSize: "20m",
maxFiles: "14d",
}),
sentryTransport,
],
});
Best Practices
-
For a deeper guide, see High-Performance Logging with pino.
-
Use
zippedArchive: trueto compress old logs — saves disk space -
Set
maxFileswith a duration suffix (14d,30d) for time-based retention -
Set
maxSizeto prevent a single day’s log from growing too large -
Use separate transports for error and info logs — errors need longer retention
-
Add a console transport in development only — file I/O slows down iteration
-
Use
winston.format.json()for structured logs — easier for log aggregators to parse -
Set
auditFileto track rotation state — helps debug rotation issues
Common Mistakes
- Not setting
maxFiles: without retention, old log files accumulate forever and fill the disk. - Setting
maxSizetoo small: frequent rotation creates many small files. 20MB is a reasonable default. - Not using
zippedArchive: uncompressed logs consume 5-10x more disk space. - Using Winston in containers: container logs should go to stdout, not files. The orchestrator handles rotation.
- Not handling
rotateevents: if you need to notify external systems of rotation, listen to the event.
FAQ
What is the difference between maxFiles as a number vs a string?
A number (maxFiles: 5) keeps exactly 5 files. A string with duration (maxFiles: "14d") keeps files from the last 14 days. Use duration strings for time-based retention.
How do I change the timezone of the date in the filename?
Set the datePattern with a timezone-aware format:
new DailyRotateFile({
filename: "logs/application-%DATE%.log",
datePattern: "YYYY-MM-DD",
// Winston uses the system timezone by default
});
For UTC, set TZ=UTC in your environment.
Can I rotate by hour instead of day?
Yes. Change datePattern:
new DailyRotateFile({
filename: "logs/application-%DATE%.log",
datePattern: "YYYY-MM-DD-HH",
maxSize: "100m",
maxFiles: "7d",
});
How do I ship rotated files to S3?
Use the archive event to trigger an upload:
const AWS = require("aws-sdk");
const s3 = new AWS.S3();
const fs = require("fs");
transport.on("archive", async (zipFilename) => {
const data = fs.readFileSync(zipFilename);
await s3.upload({
Bucket: "my-logs",
Key: `logs/${path.basename(zipFilename)}`,
Body: data,
}).promise();
});
Should I use Winston or pino for file logging?
Use pino if performance is critical (pino is 3-10x faster). Use Winston if you need multiple transports with different formats and levels — Winston’s transport system is more flexible.
Related Resources
High-Performance Logging with pino
How to use pino for fast structured JSON logging in Node.js, including log levels, child loggers, transports, and integration with Express and Fastify.
RecipeError Tracking with Sentry in Express
How to integrate Sentry for error tracking in Node.js Express applications, including error handlers, performance monitoring, release tracking, and source maps.
RecipeStructured JSON Logging with structlog
How to emit structured JSON logs in Python using structlog, including context binding, log levels, processors, and integration with standard logging.