How logrotate Works: The Execution Model
logrotate is not a daemon. It runs as a cron job, typically placed by the distribution in `/etc/cron.daily/logrotate`. On systemd-based distros, the equivalent is a timer unit:
``` systemctl status logrotate.timer ```
That timer fires once per day. logrotate reads `/etc/logrotate.conf` as its top-level config, which on most distros includes `/etc/logrotate.d/` as a drop-in directory. Each file in that directory is a stanza or collection of stanzas defining rotation rules for one or more log paths.
logrotate tracks its state in `/var/lib/logrotate/status` (path varies by distro - Debian uses `/var/lib/logrotate/status`, RHEL uses `/var/lib/logrotate.status`). This file records the last rotation timestamp per log file. If you delete it, logrotate treats every configured log as never rotated and will rotate all of them on the next run.
cat /var/lib/logrotate/status | head -20
The Configuration File Syntax
Global defaults live in `/etc/logrotate.conf`. Per-application overrides live in `/etc/logrotate.d/`. A stanza targets one or more glob patterns and wraps directives in braces.
Here is a complete annotated example for a high-volume nginx deployment:
The `missingok` directive prevents logrotate from exiting with an error if the log file does not exist - critical for services that rotate their own files or that may be stopped. `notifempty` skips rotation when the file is empty. `sharedscripts` ensures the `postrotate` block runs once even if multiple files matched the glob, rather than once per file. `dateext` appends the date instead of a numeric suffix, making archives like `access.log-20260824.gz` rather than `access.log.1.gz`.
The `create` directive sets permissions on the new empty log file logrotate creates after moving the old one. The format is `create mode owner group`. If your application drops privileges after start, the log file must be writable by the unprivileged user, so `create 0640 www-data adm` is a common pattern for nginx.
/var/log/nginx/*.log {
daily
rotate 14
size 100M
missingok
notifempty
compress
delaycompress
dateext
dateformat -%Y%m%d
create 0640 www-data adm
sharedscripts
postrotate
[ -f /run/nginx.pid ] && kill -USR1 $(cat /run/nginx.pid)
endscript
}
Rotation Triggers: time, size, and Both Together
logrotate supports four time-based triggers: `daily`, `weekly`, `monthly`, and `yearly`. These are not cron schedules - they tell logrotate how old a log must be before it qualifies for rotation on the next run. Because logrotate itself runs daily via cron, a `weekly` directive means rotation happens on the first daily run after seven days have elapsed since the last rotation.
The `size` directive triggers rotation based on file size regardless of age. `size 100M` rotates once the file exceeds 100 megabytes. `minsize` and `maxsize` add nuance: `minsize 10M` means rotate only if the file is both old enough and at least 10 MB, preventing unnecessary rotations of small files. `maxsize 500M` forces rotation even if the time threshold has not been met.
For production services generating gigabytes per day, the combination we recommend is:
``` daily size 200M rotate 30 ```
This gives you daily rotation but also triggers mid-day if a log hits 200 MB, which prevents the partition from filling between cron runs. Without the `size` directive, a traffic spike at 2am can fill your disk before the 6am cron job runs.
# Force an immediate rotation for testing
logrotate -f /etc/logrotate.d/nginx
Compression: delaycompress and Why It Matters
The `compress` directive gzip-compresses rotated logs. By default this uses gzip level 6. You can override the compression program with `compresscmd` and `compressoptions`:
``` compresscmd /usr/bin/zstd compressoptions -19 -q uncompressedcmd /usr/bin/unzstd ```
In our testing, zstd at level 19 achieves compression ratios 15-20% better than gzip on typical nginx access logs, while decompression is significantly faster - important when you are grepping through archives during an incident.
`delaycompress` is critical for applications that keep the log file descriptor open after rotation. When logrotate rotates, it moves the old log and signals the application to reopen. But some applications - particularly Java-based ones and older daemons - do not reopen immediately or buffer writes. `delaycompress` leaves the most recently rotated file uncompressed for one rotation cycle, so any straggling writes land in a readable file rather than corrupting a gzip stream.
Always use `delaycompress` with `compress` unless you have confirmed your application reopens log files immediately on the signal you send in `postrotate`.
/var/log/myapp/*.log {
daily
rotate 7
compress
delaycompress
copytruncate
missingok
notifempty
}
copytruncate vs the postrotate Signal Pattern
`copytruncate` is an alternative to the signal-based approach. Instead of moving the log file and creating a new one, logrotate copies the content to the archive and then truncates the original file to zero bytes. The application keeps its file descriptor open and never knows rotation happened.
The downside is a race condition: writes that arrive between the copy and the truncate are lost. On a high-throughput service logging 50,000 lines per second, that gap can lose thousands of log entries. For compliance-sensitive environments or security audit logs, never use `copytruncate`.
Use the postrotate signal pattern whenever possible. For nginx, the correct signal is USR1. For Apache httpd it is also USR1 (graceful). For rsyslog:
``` postrotate /usr/lib/rsyslog/rsyslog-rotate endscript ```
For applications with no signal support and no alternative, `copytruncate` is acceptable for non-critical logs like debug output. The key diagnostic question is: can you tolerate log gaps during rotation? If no, use signals. If yes, `copytruncate` is simpler.
# Check what signal your daemon expects
man rsyslogd | grep -A3 'signal'
nginx -h 2>&1 | grep -i signal
Writing postrotate and prerotate Scripts
`prerotate` runs before the log is rotated. `postrotate` runs after. Both blocks end with `endscript`. The scripts run as root by default, through `/bin/sh`.
A common pattern for PostgreSQL:
For systemd-based services, the cleanest postrotate is often just:
``` postrotate systemctl kill --kill-who=main --signal=HUP postgresql.service 2>/dev/null || true endscript ```
The `|| true` prevents logrotate from exiting non-zero if the service is down. Without this, a stopped service causes logrotate to report failure and may trigger alerts.
`firstaction` and `lastaction` are less well known. They run once before all files in the stanza are processed (`firstaction`) and once after (`lastaction`), regardless of `sharedscripts`. Useful for acquiring locks or sending a single notification after a batch of rotations completes.
If you are integrating logrotate signals into a broader DevOps automation pipeline - for example, triggering log shipping to S3 after rotation - tools like taskbotshub.ai can chain these postrotate events into multi-step workflows without requiring custom cron glue scripts.
/var/log/postgresql/*.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
create 0640 postgres postgres
sharedscripts
prerotate
# Check disk space before rotation
df -h /var/log | awk 'NR==2 {if ($5+0 > 90) exit 1}'
endscript
postrotate
systemctl kill --kill-who=main --signal=HUP postgresql.service 2>/dev/null || true
endscript
}
Running logrotate Manually and Debugging Failures
Three flags are essential for debugging:
`-d` runs logrotate in debug mode. It prints what it would do without making any changes. Combined with `-v` for verbose output, this is the fastest way to understand why a rotation is or is not triggering.
`-f` forces rotation regardless of the time threshold in the status file. Use this after editing a config to verify the postrotate script works, or after an incident where you need to rotate immediately.
`--log` writes the output to a file, useful when running logrotate via automation and you want a persistent record.
Common failure modes we have encountered:
1. **Wrong file path in the stanza** - logrotate silently skips files that do not match unless `missingok` is absent, in which case it exits non-zero. Check with `logrotate -dv /etc/logrotate.d/myapp 2>&1 | grep -i error`.
2. **Permissions on the drop-in config file** - logrotate refuses to use config files in `/etc/logrotate.d/` that are writable by anyone other than root. This is a security check. `chmod 644 /etc/logrotate.d/myapp` fixes it.
3. **postrotate script fails silently** - The postrotate block failing sets logrotate's exit code to non-zero but does not prevent rotation. Your monitoring may not catch this. Always test postrotate scripts independently.
4. **Status file timestamp skew** - If the system clock jumps backward (NTP correction on a VM after resume), logrotate may refuse to rotate because the status file shows a future timestamp. Delete the relevant line from the status file or use `-f`.
# Debug a specific config without making changes
logrotate -dv /etc/logrotate.d/nginx 2>&1
# Force rotation and log output
logrotate -vf --log /tmp/logrotate-debug.log /etc/logrotate.d/nginx
# Check exit code
echo $?
Handling High-Frequency Log Producers
Applications generating more than 1 GB per hour need logrotate supplemented with additional controls. The standard daily cron run is not sufficient.
Option 1: Move logrotate to an hourly cron.
``` # /etc/cron.hourly/logrotate #!/bin/sh /usr/sbin/logrotate /etc/logrotate.conf EXITVALUE=$? if [ $EXITVALUE != 0 ]; then logger -t logrotate "Abnormal exit with [$EXITVALUE]" fi exit $EXITVALUE ```
Pair this with `hourly` in your stanza (available since logrotate 3.9.0) and `minsize 500M` to avoid rotating tiny files every hour.
Option 2: Use `size` alone without a time directive. Logrotate will rotate whenever the file exceeds the threshold, on each run.
Option 3: For Kubernetes workloads, logrotate inside containers is generally the wrong approach. Use a sidecar with a dedicated log agent (Fluent Bit, Vector) that handles rotation natively. logrotate works best on traditional systemd hosts where it has direct access to the filesystem and can signal the parent process.
If your high-frequency log producer is a custom application you are naming and deploying, the naming and domain registration for the project can be handled through services like nicename.me before you start wiring up your logging infrastructure.
# Check logrotate version for hourly support
logrotate --version
# logrotate 3.9.0 or newer required for hourly
# Hourly stanza example
/var/log/high-volume-app/*.log {
hourly
minsize 500M
rotate 48
compress
delaycompress
missingok
notifempty
sharedscripts
postrotate
kill -HUP $(cat /run/high-volume-app.pid) 2>/dev/null || true
endscript
}
Integrating logrotate with systemd and Journald
On systems using journald, binary logs in `/var/log/journal/` are managed by journald itself via `journald.conf`, not logrotate. Do not point logrotate at journal files. Instead, configure journal retention directly:
``` # /etc/systemd/journald.conf [Journal] SystemMaxUse=2G SystemKeepFree=500M MaxRetentionSec=30day MaxFileSec=1day ```
After editing, run `systemctl restart systemd-journald` and verify with `journalctl --disk-usage`.
For services that log to both journald and a flat file (common with applications using syslog forwarding), you end up managing two retention policies. We recommend disabling file logging for any service where journald output is sufficient, reducing the logrotate surface area.
The systemd logrotate timer unit on RHEL 9 and Debian 12 runs at a randomized time within the day to avoid thundering-herd on large deployments. You can inspect and override it:
``` systemctl cat logrotate.timer # Override to run at 02:00 daily systemctl edit logrotate.timer ```
In the override, set `OnCalendar=*-*-* 02:00:00` and `RandomizedDelaySec=0` to pin the execution time.
# Override logrotate timer
mkdir -p /etc/systemd/system/logrotate.timer.d/
cat > /etc/systemd/system/logrotate.timer.d/override.conf << 'EOF'
[Timer]
OnCalendar=
OnCalendar=*-*-* 02:00:00
RandomizedDelaySec=0
EOF
systemctl daemon-reload
systemctl status logrotate.timer
Monitoring logrotate and Setting Up Alerts
logrotate does not have built-in metrics or alerting. You need to instrument it externally.
The simplest approach: wrap the cron/timer invocation in a script that checks the exit code and sends an alert on non-zero.
A more sophisticated approach uses a monitoring dead-man pattern. Push a heartbeat to a monitoring endpoint after each successful logrotate run. If the heartbeat stops, the monitor alerts. This catches cases where the cron job itself stops running, which a simple error-check script cannot detect.
For production environments, also monitor:
- Disk usage on log partitions: alert at 70%, page at 85% - Age of the newest rotated archive per critical service (if no new archive appears within 25 hours, rotation may have failed) - Size of any single log file exceeding your `size` threshold between runs (indicates size-based rotation is not triggering fast enough)
A Prometheus-based approach uses `node_exporter` with the textfile collector. Write a small script that outputs logrotate status as metrics after each run, scraped by Prometheus and alerted via Alertmanager. For teams integrating this into a larger automation framework, platforms like taskbotshub.ai can orchestrate the alert routing and incident creation without custom glue code.
#!/bin/bash
# /usr/local/bin/logrotate-monitored
/usr/sbin/logrotate /etc/logrotate.conf
EXIT=$?
if [ $EXIT -ne 0 ]; then
echo "logrotate failed with exit code $EXIT" | \
mail -s "[ALERT] logrotate failure on $(hostname)" ops@example.com
fi
# Write Prometheus textfile metric
cat > /var/lib/node_exporter/textfile_collector/logrotate.prom << EOF
logrotate_last_run_exit_code $EXIT
logrotate_last_run_timestamp $(date +%s)
EOF
exit $EXIT