How do logging.handlers.RotatingFileHandler and TimedRotatingFileHandler differ?
answer
- Two different questions about when to roll
- One measures bytes, one watches the clock
- Rollover is checked as a record arrives
- Numbered suffix versus timestamped suffix
- maxBytes times backupCount is your disk budget
basics
~20 sBoth write to one file and roll it aside, but RotatingFileHandler rolls over when the file would exceed maxBytes, while TimedRotatingFileHandler rolls over on a clock boundary chosen by when and interval. Each keeps backupCount old files.
solid answer
~40 sBoth live in `logging.handlers` and both extend `BaseRotatingHandler`, so every `emit()` first asks whether this record trips the rollover condition and, if so, rotates before writing. `RotatingFileHandler(filename, maxBytes=..., backupCount=N)` rotates on size: it shifts `app.log.1` to `app.log.2` and so on, renames the live file to `app.log.1`, and deletes whatever falls past N, giving a worst-case footprint of about `maxBytes * (N + 1)`. `TimedRotatingFileHandler(filename, when='midnight', backupCount=N)` rotates on a clock boundary and names the rotated file with a timestamp suffix rather than a number, which is far easier for a human or a shipping tool to reason about. The key shared caveat: rotation is triggered by a record arriving, not by a timer, so an idle process does not rotate.
code
python · 17 linesimport logging
import logging.handlers
import os
import tempfile
log_dir = tempfile.mkdtemp()
path = os.path.join(log_dir, "etl-export.log")
handler = logging.handlers.RotatingFileHandler(path, maxBytes=200, backupCount=2)
logger = logging.getLogger("etl.export")
logger.setLevel(logging.INFO)
logger.addHandler(handler)
for batch in range(40):
logger.info("exported batch %d", batch)
handler.close()
print(sorted(os.listdir(log_dir)))go deeper
Be ready to name both classes, say which one rotates on size and which on time, and state what maxBytes, backupCount, when and interval each control. Knowing the module they live in, logging.handlers, is part of the answer.
Explain the emit-time check: shouldRollover measures the pending record, doRollover shifts the numbered backups or writes a timestamped name. Be able to compute the worst-case disk footprint from maxBytes and backupCount.
Demonstrate that you pick the policy from how the file is consumed and from a disk budget, and that you know an idle process does not rotate. Mention utc and atTime for a fleet whose logs get correlated across regions.
Own the question of whether the application should rotate at all, or write one un-rotated file and let the platform's rotation own retention. Argue the tradeoff in terms of who is on the hook when a disk fills.
### The shared machinery Both classes live in `logging.handlers`, and both extend `logging.handlers.BaseRotatingHandler`, which itself extends `logging.FileHandler`. The base class turns every `emit()` into two steps: call `shouldRollover(record)` to ask whether *this* record trips the rollover condition, and if the answer is yes call `doRollover()` before writing the line. That single fact answers most rotation questions in an interview: **rotation is driven by records arriving, not by a background timer**. A process that logs nothing between 02:00 and 08:00 performs no rotation in that window, whichever handler it uses. With timed rotation the effect is visible as a missing or oddly-dated file for a quiet period. ### RotatingFileHandler — roll by size `RotatingFileHandler(filename, maxBytes=0, backupCount=0)` decides by measuring. `shouldRollover` formats the pending record, adds its length to the stream's current position, and returns true when the total reaches `maxBytes` — so the file rolls *before* it crosses the limit, and a single line is never split across two files. `doRollover()` then shifts the numbered backups downward: `app.log.2` becomes `app.log.3`, `app.log.1` becomes `app.log.2`, the live `app.log` becomes `app.log.1`, and whatever fell past `backupCount` is deleted. Two defaults bite people: * `maxBytes=0` disables the size check entirely — the handler behaves like a plain `logging.FileHandler` and the file grows until the disk fills. * `backupCount=0` produces no backup name at all, so nothing is preserved. Both must be non-zero for rotation to happen. The number an operator actually wants is the worst-case footprint, roughly `maxBytes * (backupCount + 1)`; choose the pair from a disk budget, not from a feeling. ### TimedRotatingFileHandler — roll by clock `TimedRotatingFileHandler(filename, when='h', interval=1, backupCount=0, utc=False, atTime=None)` computes a next-rollover instant when it opens the file and recomputes it after each rollover. `when` selects the unit — seconds, minutes, hours, days, `'midnight'`, or `'W0'`–`'W6'` for a given weekday — and `interval` multiplies it, so `when='h', interval=6` gives four files a day. The rotated file keeps the base name plus a **timestamp suffix** (`app.log.2026-09-04`) instead of a number. That is the practical reason to prefer timed rotation for anything a human reads: "yesterday's export log" is a filename, not an archaeology exercise across shifting numbers. `backupCount` here deletes the oldest matching files rather than renaming anything. Two knobs matter in production. `utc=False` is the default, so boundaries are computed in local time; a daylight-saving change therefore yields one short and one long file per year, and correlating logs across regions gets harder — set `utc=True` for a fleet. `atTime` moves the midnight or weekly boundary to a chosen time of day, which is how you keep rotation out of your busiest minute. ### They are alternatives, not two knobs on one handler You cannot ask one handler for "daily, but also cap each file at 100 MB" — the size and time policies live in different classes. Attaching both handlers to the same logger does not combine the policies either; it writes every record twice, to two different files. Pick the policy that matches how the file is consumed — size when the only constraint is disk, time when something downstream slices by day — and enforce the other bound with an operational alarm. ### Details that come up in follow-ups * `delay=True` postpones opening the file until the first record. Useful when a configuration declares many handlers of which few ever fire. * `encoding` should be set explicitly (`encoding="utf-8"`); leaving it to the platform default is how a log file becomes unreadable on another machine. * `logging.handlers.BaseRotatingHandler.namer` and `logging.handlers.BaseRotatingHandler.rotator` are hooks: `namer` chooses the rotated filename, `rotator` performs the move. Setting `rotator` to a function that compresses the file is the supported way to get gzipped archives without a second tool. * Every handler holds a `threading.Lock`, so several *threads* in one process rotate safely. Several *processes* pointed at one file do not — each has its own file object and rotates on its own. ### A worked shape ```python import logging import logging.handlers handler = logging.handlers.TimedRotatingFileHandler( "etl-export.log", when="midnight", backupCount=14, utc=True, encoding="utf-8" ) handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(name)s %(message)s")) logging.getLogger("etl.export").addHandler(handler) ``` Fourteen daily files, UTC boundaries, explicit encoding, timestamped names. If the same service had unpredictable bursts and a hard 500 MB budget, the size class with `maxBytes=100_000_000, backupCount=4` would be the honest choice instead.
- What does maxBytes=0 do on a RotatingFileHandler?It disables rotation entirely. `shouldRollover` only measures when `maxBytes` is greater than zero, so with the default of 0 the handler behaves like a plain `logging.FileHandler` and the file grows without limit. `backupCount=0` is the matching trap: no backup name is generated, so nothing is preserved either. Both values must be non-zero for size rotation to actually happen.
- A service configured with when='midnight' logged nothing for a whole day — what does the rotated file set look like?There is no file for the silent day. The rollover is performed inside `emit()`, when a record arrives and the handler notices the boundary has passed — nothing runs on a timer. The next record after the quiet period triggers one rollover and then writes, so you get a gap in the timestamp sequence rather than an empty dated file.
- How do you get rotated files compressed without adding an external tool?Set the `logging.handlers.BaseRotatingHandler.rotator` hook on the handler instance to a callable taking `(source, dest)` that compresses instead of renaming, and set the `namer` hook to append a `.gz` suffix to the destination name. The handler calls those instead of its own rename, so the compression runs inline in the rotating process — fine for modest files, worth moving off-process for large ones.
saying these in an interview costs you the question
- Thinks a background timer thread performs the rotation
- Expects a timed handler to rotate while the process is idle
- Believes maxBytes=0 means rotate on every record
- Reads backupCount as a total size or day budget
- Sets both a size and a time policy on one handler
- Attaches both handlers to one logger and is surprised by duplicate lines