Vintage carousel with wooden duck figures perched on logs instead of traditional horses, illuminated by strings of lights, at a fairground with tents and rides visible in the background.

These three decide when the logging collector closes one file and opens another, and they only make sense together with log_filename, because the collector never deletes anything: rotation is just “compute a name from the pattern and open it.” What happens next depends entirely on whether that name is new, and the three parameters here control the timing and one detail of the opening. Two of the three are misnamed, one is a trap in the documentation’s own example, and the mechanics are short enough to lay out completely.

log_rotation_age

Default 1d, units minutes, context sighup, 0 disables it. The name says elapsed time, and the documentation calls it “the maximum amount of time to use an individual log file,” which is true and leaves out the interesting part. The collector computes the next rotation as the next clock time that is a whole multiple of the interval, counted from the Unix epoch, in log_timezone. With 1d, that’s local midnight, whenever the server started. With 1h, it’s the top of the hour. In the sandbox I set it to one minute at 02:53:05 and the next file appeared at 02:54:00, not 02:54:05.

Two consequences follow. A value like 7d rotates at midnight on Thursdays, because the epoch was a Thursday, and there is no way to ask for Sundays. And the file that opens at a boundary is named for the boundary, not for the moment the collector got around to it (the source calls the alternative “slippage”), so a daily file named %Y-%m-%d is named for the day it covers even if the collector woke a few seconds late. The boundary is computed with the UTC offset in effect at the time of computing, so twice a year, on the DST days, the rotation lands an hour off and corrects itself at the next one.

The collector honors this even on an idle server: it sleeps until the next boundary, not until the next message. If rotation isn’t happening at all, it’s disabled, which the collector does to itself after any failure to open a new file other than running out of descriptors (see the log_directory post), and a reload turns it back on.

log_rotation_size

Default 10MB, units kB, context sighup, 0 disables it. Each format’s file is checked separately against the limit (.log, .csv, .json each get their own size rotation), and a rotation triggered by one doesn’t touch the others, whereas a time-based rotation rotates all of them at once.

This is the parameter that interacts badly with a sensible filename. Size rotation computes the new name from the current time. If log_filename only changes daily, the name is the one already open, and the collector closes the file and reopens it in append mode, which accomplishes nothing, and then does it again on the next pass through its loop, because the file is still over the limit. In the sandbox, a minute-named file with log_rotation_size = '32kB' grew to 357 kB without a second file ever appearing. The documentation’s advice to include %M in the pattern is this: size rotation is only meaningful if the name can change more often than the clock rotates it.

So decide which regime you’re in. If you want one file per day, size rotation has nothing to offer and you set it to 0. If you want files capped at a size, the pattern needs at least %H%M in it, and you’ve given up on a predictable file per period. The default pattern, with seconds in it, works with both, which is the only reason the default size limit doesn’t misbehave out of the box.

log_truncate_on_rotation

Default off, context sighup. When on, the collector opens the new file with w instead of a, subject to three conditions that the documentation states as one: the rotation was time-based, not size-based or forced by pg_rotate_logfile() or pg_ctl logrotate; the computed name differs from the file currently open; and this isn’t the first file after a start. The last two are the same rule from different angles: never truncate the file you’re already writing to, and never truncate at startup, because the file with today’s name might be the one from before the restart. I checked both. A same-named file kept its contents across a restart with truncation on, and a pre-existing file with the next minute’s name was emptied at the boundary.

This parameter exists for one configuration: a cyclic name, postgresql-%a.log with 1d, giving seven files that overwrite themselves a week later, or %H with 1h for twenty-four. That’s the only built-in retention PostgreSQL has, and it works because truncation reclaims the space before the collector writes into a name it has used before.

Now look at the documentation’s example, which combines a cyclic hourly name with %M for size rotations: server_log.%H%M, log_rotation_age = 60, log_rotation_size = 1000000, truncation on. The hourly files (server_log.1300) are truncated at 13:00 the next day, as intended. A file created by a size rotation at 13:37 (server_log.1337) is opened by a size rotation, which never truncates, and is never the target of a time-based rotation, which happens only on the hour. It is never truncated. It’s appended to the next time a size rotation happens to land in the same minute, and otherwise sits there until someone deletes it. The example’s retention leaks through the very parameter it added.

What to do

Take the two-regime choice from the log_filename post and finish it here.

If something outside the server compresses and deletes log files (and something should), then: log_filename = 'postgresql-%Y-%m-%d.log', log_rotation_age = 1d, log_rotation_size = 0, log_truncate_on_rotation = off. One complete file per day, named for the day, closed at local midnight, and the reaper does the rest.

If nothing outside the server will ever touch the directory, then: postgresql-%a.log, 1d, 0, on. Seven files, one week of history, no size cap, and accept that a bad day can produce a very large Tuesday.

What not to do is mix them. Size rotation plus truncation is the leaky example above; size rotation plus a daily name is a file that grows forever while the collector reopens it on every pass; the default pattern with a size limit and no reaper is an unbounded directory of ten-megabyte files, which is the configuration that ships, and it’s the reason log/ is where disks go to fill up.