An administrator on a bog-standard Debian 13 machine wonders why yesterday's access log is still lying about uncompressed, opens /etc/logrotate.d/nginx, and finds eighteen lines. A block in braces, eight keywords, a prerotate script that runs whatever somebody has left in a directory called httpd-prerotate, and a postrotate script that asks the init system to pass a message on to nginx. The file came with the nginx package, it works, and in most cases nobody has opened it since the day it arrived.
On FreeBSD the same arrangement is one line:
/var/log/nginx/*.log 644 14 * @T00 GJE /var/run/nginx.pid SIGUSR1
It goes into a file of its own under /usr/local/etc/newsyslog.conf.d/, a directory the stock configuration already includes, and that is the whole of the setup. There is nothing to install and no timer to enable. The program that reads the line is newsyslog, which Theodore Ts'o wrote at MIT's Project Athena in 1987 and which has been part of the FreeBSD base system since 1996.
Nine fields and no script
Read from left to right, the line says: these files, mode 644, keep fourteen old copies, ignore the size (that is the asterisk), rotate at midnight (@T00), treat the name as a glob (G), compress the old copies (J), leave an empty file alone (E), and afterwards send SIGUSR1 to the process whose ID is in /var/run/nginx.pid. That path is the one the nginx port compiles in. The signal could just as well be written as its number, 30, and the manual page says so in as many words.
Each keyword in the Debian block has its field or flag here. missingok is simply the default, since newsyslog skips a file that does not exist. create happens on its own, with the mode from the line. notifempty is the E. sharedscripts, which stops logrotate from signalling a daemon once per file, has no work to do on FreeBSD: newsyslog collects its signals first and sends each one once per PID file, however many logs stand behind it, because save_sigwork() compares the file names before it queues anything. The prerotate hook has no counterpart, and THE LIMIT comes back to it.
FreeBSD uses the same form for its own daemons. The base system ships five such files in /etc/newsyslog.conf.d/, a single line each, and the one for pf reads:
/var/log/pflog 600 3 1000 * JB /var/run/pflogd.pid
That is the packet log, kept as three old copies, rotated at 1,000 kilobytes, marked binary with B so that newsyslog writes no text into it, and pflogd told by the default signal, SIGHUP, to start a fresh file.
Rename, signal, wait
The nginx documentation describes log rotation in two sentences. The files "need to be renamed first. After that USR1 signal should be sent to the master process." The master reopens its logs, the workers follow, and the old files are free for compression a moment later.
newsyslog does precisely that, in that order, for every file that is due. It renames them all and works through its queue of signals. Then it waits ten seconds before compressing anything, and its verbose output gives the reason in plain words: "Pause 10 seconds to allow daemon(s) to close log file(s)". Ten seconds is a fair bit longer than nginx needs, mind, and for a daemon that takes its time there is the flag P, Debian's delaycompress in a single letter, which leaves the newest old copy uncompressed until the next run compresses it.
logrotate reaches the same result by another road. Its postrotate script goes to execl("/bin/sh", "sh", "-c", ...), line 543 of logrotate.c in 3.22.0, and the Debian block adds delaycompress, which leaves the newest old log uncompressed until the following run, a day later, in case the daemon is still writing into it (which answers the administrator from the opening). For programs that cannot be told to reopen their log at all there is copytruncate, which copies the file and empties the original. The logrotate manual is candid about the price: "there is a very small time slice between copying the file and truncating it, so some logging data might be lost."
FreeBSD's /etc/crontab starts newsyslog at minute zero of every hour, so a size limit is checked twenty-four times a day, and a line that names both a size and a time is rotated by whichever comes first; in the source, an oversized file skips the time check altogether (lines 559 and 592 of newsyslog.c). The default configuration uses exactly this for /var/log/messages, rotated at 1,000 kilobytes or on 1 January. On a Debian system with systemd, logrotate.timer starts logrotate with OnCalendar=daily (without systemd, /etc/cron.daily does the same), and its manual adds that it "will not modify a log more than once in one day unless the criterion for that log is based on the log's size and logrotate is being run more than once each day". A runaway log on a stock Debian machine is therefore caught by the next daily run, whereas on FreeBSD it has an hour at most.
A manual from 1989
The manual page that FreeBSD imported is dated 12 January 1989 and signed by Project Athena. It describes a program that "should be scheduled to run periodically by crontab", and then makes a promise: "Since the program is quite fast, it may be scheduled to run every hour without any ill effects." FreeBSD took it at its word. Thomas Graichen imported newsyslog from NetBSD on 5 January 1996, with a commit message that explains the motive rather well, since until then the rotation "is currently done by "hand" in the /etc/[daily,weekly,monthly] scripts". The hourly line in the crontab has been there ever since.
The newsyslog.c of 1996 had 568 lines. Garance Drosehn reworked it substantially in 2003 and 2004, and the directory has seen 300 commits up to releng/15.1, which brought globbing, include files, signals by name, a choice of compressors and archive names with timestamps. In 1989 the configuration line had "five mandatory fields and two optional fields"; today it has five mandatory fields and four optional ones, in the same order, and the parser still accepts the full stop between owner and group that the 1989 page called "essential" (line 1252). Apple ships the program too: macOS 27.0.1 still carries /usr/sbin/newsyslog.
The sheet
Efficiency. /usr/sbin/newsyslog is 48,760 bytes on FreeBSD 15.1-RELEASE for amd64 and links against libc plus two small libraries of the base system, libsbuf and libutil. Its source is 2,598 lines of C without comments and blank lines: 2,154 in newsyslog.c, 418 in ptimes.c and 26 in extern.h. logrotate 3.22.0, counted the same way, has 4,878 lines in its C sources; its binary in Debian 13 is 92,328 bytes and links against libacl, libselinux, libpopt and libc, and the package depends on cron or systemd for its schedule. FreeBSD's default configuration rotates sixteen system logs in sixteen lines, plus the five one-line files for lpr, opensm, pf, ppp and sendmail.
Security. Of the four questions: newsyslog runs as root from cron; it reads no data from the network; it parses configuration files that only root can write; and it does not drop its privileges, which is why those files must stay root's. Of FreeBSD's 729 security advisories none concerns newsyslog, and the two that concern syslogd date from 1996 and 2001. NVD lists seventeen entries that name logrotate, four of them in its own code (three from 2011, one from 2022) and the rest in configurations that other packages ship for it.
Longevity. Written in 1987 by Theodore Ts'o at MIT Project Athena, in FreeBSD since 5 January 1996 and first released with 2.2, maintained in the base system ever since, and also present in NetBSD and macOS. logrotate's changelog begins with 2.0.1 on 9 December 1996.
Stability. The five mandatory fields have kept their order since 1989, the optional fields added since sit at the end, and owner.group with a full stop still parses. The working minimum for a daemon's log is one line.
Measured on 22 October 2026: source lines without comments and blank lines in FreeBSD releng/15.1 (usr.sbin/newsyslog) and logrotate at tag 3.22.0; binary sizes and linked libraries from the FreeBSD 15.1-RELEASE base.txz for amd64 and the Debian 13 package logrotate_3.22.0-1_amd64.deb (ls -l, objdump -p); /etc/crontab and the newsyslog configuration with its directory /etc/newsyslog.conf.d/ from the same base.txz; the nginx block from Debian 13's nginx-common_1.26.3-3+deb13u7; macOS 27.0.1 (26A434) with ls -l /usr/sbin/newsyslog.
The limit
Four limits, and the first is the one a Linux administrator will raise.
No copytruncate. newsyslog moves the file and relies on the daemon to reopen it, prompted by a signal or, with the R flag, by a command. A program that can be told neither has no way out here, and logrotate's copytruncate serves it at the price its own manual names.
The R flag runs a command as root. newsyslog hands that field to system() (line 2107), so whoever can write a line into one of the included directories can run commands as root on the hour. CVE-2026-57155 is that case: in OPNsense a path traversal let an administrator of firewall aliases write files as root, and a crafted newsyslog fragment turned the write into command execution. The include directories of the base system belong to root:wheel with mode 755, and they have to stay that way.
Fewer hooks. logrotate brings prerotate, firstaction, lastaction, the mailing of old logs and a good deal more. newsyslog brings a signal or a command, and anything beyond that is a small script of one's own.
On Debian the system log has moved. Since Debian 12 rsyslog is no longer installed by default and the system log lives in the journal, so logrotate's daily work there is mostly the files of applications such as nginx, which is precisely where the eighteen lines from the opening live.
In thirty-seven years the line has grown two fields longer, and both were added at the end.