Linux and Bash for Developers · Files, Disks and Permissions
Logs Rotate but the Disk Keeps Filling
Understand why moving a log file doesn't stop a process writing to it, and rotate logs correctly with logrotate.
Scenario
A team adds a nightly job that renames app.log to app.log.1 and compresses old copies. A week later the disk is full again: app.log is tiny, but app.log.1 keeps growing, and the compressed copies are empty.
What you see in production
- The "rotated" file keeps growing after rotation.
- The new
app.logstays empty or is never created. - Compressed archives are created from files still being written to.
The naive code
mv app.log app.log.1
gzip app.log.1
touch app.log
Reproduce it
A writer keeps an open descriptor; renaming the file doesn't change which inode the descriptor points at:
python3 - <<'EOF' &
import time
log = open("app.log", "a", buffering=1)
for i in range(6):
log.write(f"request {i}\n")
time.sleep(0.3)
EOF
pid=$!
sleep 0.5
mv app.log app.log.1
touch app.log
wait $pid
echo "app.log: $(wc -l < app.log) lines"
echo "app.log.1: $(wc -l < app.log.1) lines (the app kept writing here)"
Output
app.log: 0 lines app.log.1: 6 lines (the app kept writing here)
Run this on your computer — see “Get Started” for setup.
Root cause
An open file descriptor refers to an inode, not a name. mv changes the name; the process keeps writing to the same inode, now called app.log.1. Until the process reopens its log file, the new app.log is never written.
The fix
Either tell the process to reopen its logs after rotating (best), or copy and truncate in place (copytruncate) for apps that can't reopen. logrotate does both:
mkdir -p logs
python3 - <<'EOF' &
import time
log = open("logs/app.log", "a", buffering=1)
for i in range(8):
log.write(f"request {i}\n")
time.sleep(0.25)
EOF
pid=$!
cat > logrotate.conf <<EOF
$PWD/logs/app.log {
rotate 7
daily
missingok
notifempty
copytruncate
compress
delaycompress
}
EOF
sleep 0.9
/usr/sbin/logrotate --force --state "$PWD/logrotate.state" logrotate.conf
wait $pid
ls logs
echo "app.log.1: $(wc -l < logs/app.log.1) lines, app.log: $(wc -l < logs/app.log) lines (new writes land here)"
Output
app.log app.log.1 app.log.1: 4 lines, app.log: 4 lines (new writes land here)
Run this on your computer — see “Get Started” for setup.
- For apps that support it, use
postrotatewith a reload signal (for examplesystemctl reload nginxorkill -USR1) instead ofcopytruncate. - Better still for services: log to stdout, and let journald, Docker or Kubernetes handle storage and rotation.
Trade-offs
copytruncatecan lose lines written between the copy and the truncate.- Reopen-on-signal is lossless but needs app support.
- Shipping logs to a central system (Loki, Elasticsearch, CloudWatch) reduces local retention needs.
Detect it in production
- Alert on disk growth rate and on files under
/var/loglarger than expected. - Check rotated files' timestamps: a
.1file with a recent modification time means the app is still writing to it. logrotate --debug /etc/logrotate.d/appshows what would happen without rotating.
Key points
- Processes write to inodes; renaming a file doesn't redirect their writes.
- Rotate with logrotate: reopen via postrotate, or copytruncate if you must.
- For services, log to stdout and let the platform rotate.
Further reading: logrotate(8) · systemd journald
Practice questions
Why does a process keep writing to app.log.1 after you rename app.log?
Answer
Its open file descriptor points to the inode, not the name; renaming doesn't change which file it writes to.
What's the trade-off of logrotate's copytruncate?
Answer
It works with apps that can't reopen logs, but lines written between the copy and the truncate can be lost.
What's the simplest logging approach for services on modern platforms?
Answer
Write logs to stdout/stderr and let journald, Docker or Kubernetes collect and rotate them.
Open in the app to mark lessons complete, take notes and track your progress.