Loguru Rotation, Retention and Compression

Loguru's file sink is the reason many teams pick it: rotation, retention and compression are three keyword arguments rather than three classes. This page covers what those arguments actually do, the timing behaviour that surprises people, and the deployment shape where in-process rotation stops being safe. It builds on Loguru configuration and sinks, part of the modern Python logging libraries deep dive section.

One file's life: active, rotated, compressed, deleted A log file's lifecycle drawn as four stages. In the active stage the sink appends records to app.log and, on every write, checks whether the rotation condition is satisfied. When it is, the file is renamed with a timestamp suffix and a fresh app.log is opened, so the rotation boundary always lands just after the threshold rather than exactly on it. If compression is configured, the rotated file is then compressed to app.log.gz, which happens inline in whichever thread performed the write unless the sink is enqueued. Finally the retention policy runs, globbing the directory and deleting the files that exceed the configured count or age. The diagram marks that each of the three settings is independent: rotation without retention fills the disk more slowly, retention without rotation never fires because there is only ever one file, and compression without either does nothing at all. rotation · compression · retention — three independent settings 1 · active app.log condition checked on every write 2 · rotated app.2026-08-02.log renamed, new file opened just after the threshold 3 · compressed app.2026-08-02.log.gz inline in the writing thread unless enqueue=True 4 · deleted by retention globs the directory by count or age what each one does on its own rotation without retention — the disk fills more slowly, and still fills retention without rotation — never fires, because there is only ever one file to keep compression without rotation — nothing to compress; the active file is never compressed they are only useful together, which is why the sink takes all three in one call
Retention is the one that actually bounds disk usage. Rotation on its own just changes how the space is divided up.

Prerequisites

pip install "loguru>=0.7.2,<1.0.0"
export LOG_DIR=/var/log/checkout
export LOG_ROTATION="100 MB"
export LOG_RETENTION="14 days"

Implementation

Step 1 — Configure one sink with all three settings. logger.add() returns a sink ID; logger.remove() with no argument clears the default stderr sink, which is almost always the first line of a production setup.

import os
import sys
from loguru import logger

logger.remove()                                   # drop the default stderr sink

logger.add(
    f"{os.environ['LOG_DIR']}/app.log",
    rotation=os.environ.get("LOG_ROTATION", "100 MB"),
    retention=os.environ.get("LOG_RETENTION", "14 days"),
    compression="gz",
    enqueue=True,                                 # writes and rotation on a dedicated thread
    serialize=True,                               # one JSON object per line
    level="INFO",
    backtrace=False,                              # do not expand frames in production
    diagnose=False,                               # never render variable values
)

diagnose=False is not optional in production. With it enabled, Loguru's exception rendering includes the values of local variables in each frame, which will eventually put a password, a token, or a customer record into a log file.

Step 2 — Choose the rotation trigger deliberately. Loguru accepts three shapes, and each answers a different question.

rotation="100 MB"        # size — predictable disk usage, unpredictable time boundaries
rotation="00:00"         # daily at midnight — predictable boundaries, unpredictable size
rotation="1 week"        # duration since the file was created
rotation=lambda msg, file: file.tell() > 5e8 and msg.record["level"].no >= 40  # custom

The timing detail matters: the condition is checked on write, not on a timer. A "00:00" rotation on a service that receives no traffic between 23:50 and 06:00 rotates at 06:00, and the resulting file is labelled with the time it was rotated. If exact daily boundaries matter — for a compliance export, say — rotate externally with logrotate and let Loguru simply append.

Step 3 — Make retention actually delete. Retention accepts a count, a duration, or a callable. It runs after each rotation and globs the sink's directory.

retention=10             # keep the ten most recent files
retention="14 days"      # delete anything older
retention=lambda files: [os.remove(f) for f in sorted(files)[:-10]]   # explicit

Point the sink at a directory nothing else writes to. Retention selects candidates by pattern-matching the sink's filename, and a shared /var/log directory can contain files that match by accident — a rotated file from a previous deployment, for instance.

Size or time — you can have predictable sizes or predictable boundaries One week of traffic under two rotation triggers. With a size trigger of one hundred megabytes, every file is the same size but the boundaries fall wherever traffic happens to reach the threshold: three files on the busy Monday, one spanning the whole quiet weekend, and no relationship between a file and a calendar day. With a daily time trigger, every file covers exactly one day, so finding Tuesday's records means opening Tuesday's file, but the sizes vary by an order of magnitude between the busiest weekday and the quietest weekend day, and a burst can produce a single very large file. The choice follows from how the files are consumed: size when disk budget is the constraint, time when someone will open them by date. one week of traffic, two triggers rotation="100 MB" every file is 100 MB — three on Monday, one across the whole weekend predictable disk, unpredictable boundaries rotation="00:00" Mon Tue Wed Thu Fri Sat Sun one file per day — but Saturday is a tenth of Monday size when the disk budget is the constraint · time when someone will open a file by date and remember the check runs on write, so a quiet night rotates in the morning, not at midnight
Neither trigger is better. They fail differently: size loses the calendar, time loses the size budget on the day a burst arrives.

Step 4 — Enqueue the sink. enqueue=True moves every write, rotation and compression to a dedicated thread and makes the sink safe across processes that Loguru itself spawned. It is also what keeps a 200 MB gzip from happening on a request thread.

logger.add("/var/log/checkout/app.log", rotation="100 MB", compression="gz", enqueue=True)

The cost is a queue and a thread, and one obligation: call logger.complete() (or logger.remove()) at shutdown so the queue drains before the process exits. The async-specific behaviour of this flag is covered in async logging with Loguru's enqueue.

Step 5 — Stop rotating when there is more than one process. Two gunicorn workers with the same file sink will both decide the file is too big and both rename it. Records written after the first rename go to a file the second worker has already moved.

# under gunicorn, uvicorn --workers, or a Celery prefork pool:
logger.remove()
logger.add(sys.stdout, serialize=True, level="INFO", enqueue=True)   # no files, no rotation

Let the container runtime, systemd, or logrotate own the files. This is not a Loguru limitation — the standard library's RotatingFileHandler has exactly the same problem, and for the same reason.

What happens when two processes rotate the same file Two worker processes with file sinks pointing at the same path. Both append records, and both check the rotation condition on every write, independently. Worker one reaches the threshold first, renames the file to a timestamped name and opens a fresh one. Worker two, whose open file handle still refers to the renamed file, has not yet checked its own condition and continues appending to the file it already holds — so its records land in the rotated file rather than the new one, and then worker two rotates as well, renaming the fresh file that worker one is now writing to. The result is interleaved rotations, records in files that appear to predate them, and a retention policy deleting files that are still being written. The fix shown is to log to stdout under multiple processes and let the platform own the files. two workers, one path, two independent rotation checks worker 1 holds app.log worker 2 holds app.log threshold reached → rename opens a fresh app.log still writing to its handle which is now the renamed file what the directory ends up holding records in files that predate them interleaved rotation timestamps retention deleting a live file and no error, anywhere the fix is not a lock log to stdout and let the container runtime, systemd or logrotate own the files — one writer per file, always
Nothing errors. The records are written, to the wrong files, and a retention sweep eventually deletes one that a worker is still appending to.

Configuration options

Option Type Default Recommended
rotation str / int / callable none "100 MB" or "00:00"
retention str / int / callable none "14 days" or a file count
compression str / callable none "gz"
enqueue bool False True
serialize bool False True for machine-read logs
backtrace bool True False in production
diagnose bool True False — it renders variable values
catch bool True True — a sink error must not raise
Multi-process stdout only; no in-process rotation

Verification

from loguru import logger
import os, pathlib

logger.remove()
logger.add("/tmp/rot/app.log", rotation="1 KB", retention=3, compression="gz", enqueue=True)

for i in range(2000):
    logger.info("record {}", i)

logger.complete()                    # drain the queue before inspecting
print(sorted(p.name for p in pathlib.Path("/tmp/rot").iterdir()))

Expected Output:

['app.2026-08-02_13-41-02_884321.log.gz',
 'app.2026-08-02_13-41-02_913776.log.gz',
 'app.2026-08-02_13-41-02_941208.log.gz',
 'app.log']

Three compressed files plus the active one — retention deleted the rest. Two things to check specifically: the count matches retention, and the rotated files are .gz rather than plain, which confirms compression ran rather than silently doing nothing.

Common mistakes

The disk fills anyway

Error signature: rotation is clearly working — dozens of files — and the volume is full. Root cause: rotation was set and retention was not, so nothing is ever deleted. Remediation: always set both. Rotation divides the space; only retention bounds it.

Records go missing under gunicorn

Error signature: a worker's records stop appearing after a rotation, and the file sizes do not add up. Root cause: multiple processes rotating the same path independently. Remediation: log to stdout under multiple processes and let the platform handle files.

A latency spike every time a file rotates

Error signature: a periodic P99 spike whose interval matches the rotation cadence. Root cause: compression runs inline in the writing thread. Remediation: set enqueue=True, which moves rotation and compression to the sink thread.

Passwords in the log file

Error signature: an exception block in the log contains local variable values, one of which is a credential. Root cause: diagnose=True, Loguru's default, renders frame locals in tracebacks. Remediation: set diagnose=False and backtrace=False in every non-development configuration, and pair it with the redaction described in redacting sensitive data in log records.

Files or stdout

Rotation only matters if files are the right destination, and in a containerised deployment they usually are not. The decision is worth making explicitly rather than inheriting from whichever example the project started from.

Stdout is right when the platform collects it: a container runtime with a logging driver, systemd with the journal, a supervisor that pipes to an agent. In that arrangement the process writes to a pipe and something else owns durability, rotation and retention — all of which it does better, because it can do them across every process on the host rather than per process. The application configuration reduces to one sink and no file management at all.

Files are right when there is no collector, when the log has to survive the loss of the network, or when an on-host process reads it — a security agent tailing an audit file, a legacy shipper, a support tool. They are also right in a single-process deployment where the operational simplicity of "the log is at this path" outweighs everything else.

Both is a mistake more often than it looks. Writing to stdout and a file doubles the I/O, doubles the storage, and creates two sources of truth that will eventually disagree about what was retained. If a file is needed for one specific consumer, give that consumer its own sink with its own level and format rather than duplicating the whole stream.

Deployment Sink Rotation owned by
Container with a logging driver stdout the platform
Kubernetes with a node agent stdout the agent
systemd unit stdout the journal
Bare host, no collector file Loguru, or logrotate
Single process, on-host consumer file Loguru
Multiple processes, one path stdout never Loguru

Retention as a policy, not a setting

retention="14 days" is a number in a configuration file, and the number usually comes from nowhere in particular. Three inputs decide it, and they are worth writing down next to the setting.

The first is how far back an investigation realistically reaches. For most services this is days rather than weeks: an incident is investigated while it is fresh, and a question asked a month later is asked of metrics rather than logs. The second is any compliance requirement, which is usually longer and usually applies to a specific subset — audit and access records — rather than to everything. The third is cost, which is what actually constrains the first two.

The productive shape that follows is different retention for different streams rather than one number for everything: a short window for the high-volume operational stream, and a long one for the low-volume audit stream in its own sink. Loguru makes that straightforward, because a sink is a per-destination configuration.

logger.add("/var/log/app/app.log", rotation="100 MB", retention="7 days",
           compression="gz", enqueue=True, level="INFO")

logger.add("/var/log/app/audit.log", rotation="1 day", retention="400 days",
           compression="gz", enqueue=True, level="INFO",
           filter=lambda record: record["extra"].get("audit") is True)

The filter argument is what makes the second sink cheap: it receives only the records explicitly marked as audit events, so its volume is a tiny fraction of the first and a year of retention costs less than a week of the operational stream.

Verifying retention actually runs

Retention executes after a rotation, which means a sink that never rotates never deletes anything. A service quiet enough that its file has not reached the size threshold in a month has a month of records in one file and a retention setting that has never executed. Checking the directory occasionally is the whole test — a file count that is not what the retention setting implies, or an active file far older than the rotation interval suggests, is the symptom.

Frequently Asked Questions

When does Loguru check the rotation condition?

On write. There is no background timer: each record that reaches the file sink triggers a check of whether the rotation condition is now satisfied. A size trigger therefore fires slightly after the threshold, and a daily time trigger fires on the first record after midnight rather than at midnight — so a service that goes quiet overnight rotates late, and one that stops entirely does not rotate at all.

Does retention delete files Loguru did not create?

It can. Retention globs the sink's directory using the pattern derived from the filename, so any file matching that pattern is a candidate — including one left by a previous deployment with a different naming scheme, if it happens to match. Point the sink at a directory it owns, rather than at a shared log directory.

Is compression worth it?

For text logs, yes — gzip typically reduces them by 80 to 95 percent, and rotated files are never read at speed. The cost is that compression runs when the rotation happens, in the same thread as the write unless the sink is enqueued, so a large file can produce a visible pause. With enqueue set to True that pause moves to the sink thread and stops mattering.

Can two gunicorn workers write to the same Loguru file sink?

They can write, and it will mostly work for individual lines because each write is small. What breaks is rotation: two processes independently decide the file is too large and both rename it, so one worker's records go to a file that has already been moved. Log to stdout under multiple processes and let the container runtime or a supervisor own the files.