Vivian Voss

periodic

unix freebsd linux devops

The Unix Way ■ Episode 12

You have inherited a server. Congratulations. It runs seventeen cron jobs. Three were written by someone who left in 2019. One backs up a database that was decommissioned last March. Two conflict with each other at 02:15 every Sunday. Nobody knows what the remaining eleven do, because cron does not tell you whether a job succeeded. It tells you it ran. Marvellous distinction, that.

cron is a clock with a trigger. It fires and forgets. Scatter your jobs across per-user crontabs and root's crontab, and within six months you have a system held together by faith, habit, and the vague hope that no one touches it. This is not a theoretical scenario. This is every production server one has ever inherited.

The BSDs looked at this and asked a rather sensible question: what if the system told you what happened?

FreeBSD: periodic(8)

periodic(8) is a framework. Not a scheduler, not a daemon, not a service. A framework that sits on top of cron and provides structure, configuration, and output management for recurring system tasks. FreeBSD built it in the 1990s. It has worked without drama since.

The architecture is straightforward. Scripts live in directories:

/etc/periodic/daily/
/etc/periodic/weekly/
/etc/periodic/monthly/
/etc/periodic/security/

Each script is a self-contained shell script that performs one task and exits with a status code:

periodic Exit Codes: Severity-Based Output 0 nothing notable suppressible 1 information suppressible 2 config warning suppressible >2 critical always shown Output collected from all scripts, filtered by severity, delivered as one report. One email every morning. You read it with your coffee and know the state of the machine. Adding a custom task: drop a script into the directory. That is the entire API. No registration. No reload. No daemon restart. The framework discovers it on the next run.

One configuration file controls the entire system:

# /etc/periodic.conf
daily_clean_tmps_enable="YES"
daily_backup_passwd_enable="YES"
daily_status_security_enable="YES"
daily_accounting_enable="YES"
daily_show_success="NO"
daily_show_info="YES"
daily_show_badconfig="YES"
daily_output="/var/log/daily.log"

Enable a task: set it to "YES". Disable it: "NO". Change the output destination: set daily_output to a file path or an email address. One file. Every task. Every setting. The pattern is identical to rc.conf: system defaults are never modified, local changes are applied on top.

The output is collected from all scripts, filtered by severity, and delivered as a single report. If daily_output is an email address, you receive one email every morning with the complete state of your system: which temp files were cleaned, whether the password database was backed up, what the security check found, and whether anything went wrong. You read one email with your morning coffee and know the state of the machine.

Adding a custom task is trivial: write a shell script, make it executable, drop it into /etc/periodic/daily/. That is the entire API. No registration, no configuration reload, no daemon restart. The framework discovers it on the next run. One does not recall the last time an API consisted of "put a file somewhere."

newsyslog(8) already knows about /var/log/daily.log, /var/log/weekly.log, and /var/log/monthly.log. The logging of your maintenance framework is itself maintained. One does appreciate the recursion.

cron still does the scheduling. Three lines in /etc/crontab:

0  2  *  *  *  root  periodic daily
0  3  *  *  6  root  periodic weekly
0  5  1  *  *  root  periodic monthly

cron triggers periodic. periodic runs the scripts. The scripts report their status. The framework collects the output. The administrator reads one email. The separation of concerns is, one must note, rather elegant.

OpenBSD: daily(8)

OpenBSD takes a characteristically minimal approach. Three shell scripts ship with the base system: /etc/daily, /etc/weekly, and /etc/monthly. These are system scripts. You never modify them.

Your additions go into /etc/daily.local, /etc/weekly.local, and /etc/monthly.local. These local scripts run first, before the system scripts, which makes it convenient to define variables, perform cleanup, or prepare state that the system scripts depend on.

The daily script performs a comprehensive set of checks: removes scratch files from /tmp, purges accounting records, checks daemon status (lists any daemons enabled in rc.conf.local that are not actually running), reports which file systems need to be dumped, runs the security(8) check script, and optionally backs up the root file system to /altroot.

The rather splendid part: the daily script reports processes killed by pledge(2) and unveil(2) violations. Software that attempted to exceed its declared capabilities or access files it had no business accessing. Security is not a product you install. It is not an agent you deploy. It is a shell script that runs every morning and tells you who misbehaved. Quite civilised.

Linux: systemd timers

Linux offers systemd timers as the modern alternative to cron. Each scheduled task requires two files: a .service unit file defining what to run, and a .timer unit file defining when to run it.

# /etc/systemd/system/cleanup.service
[Unit]
Description=Daily cleanup

[Service]
Type=oneshot
ExecStart=/usr/local/bin/cleanup.sh
# /etc/systemd/system/cleanup.timer
[Unit]
Description=Run cleanup daily

[Timer]
OnCalendar=daily
Persistent=true

[Install]
WantedBy=timers.target

Then enable and start it:

systemctl daemon-reload
systemctl enable --now cleanup.timer
10 Maintenance Tasks: File Count FreeBSD periodic 1 config + 10 scripts in directory one daily email report OpenBSD daily 3 + 3 system + local scripts pledge/unveil reports Linux systemd 20 files 10 services + 10 timers + 10 reload commands FreeBSD: collected daily report, severity filtering, one config file. OpenBSD: security baked into the daily routine. Linux: each task in its own universe. No collected report.

For ten maintenance tasks, this produces twenty files and ten reload-and-enable sequences. The output goes to the journal, retrievable via journalctl -u cleanup.service. There is no collected daily report. There is no severity filtering. There is no single email summarising the state of the system. Each task lives in its own universe.

systemd timers do offer features that cron does not: monotonic timers (relative to boot), persistent timers (catch up missed runs), calendar expressions with second-level granularity, and dependency ordering. These are genuine capabilities. Whether the complexity is justified for "run this script every morning" is, one suspects, a matter of taste. And file count tolerance.

The Point

cron tells you when. periodic tells you what happened.

The difference between a scheduler and a maintenance framework is the difference between "the job ran" and "the job ran, succeeded, found nothing unusual, and here is the proof." One of those lets you sleep. The other requires you to check.

FreeBSD built a framework: structured directories, a single configuration file, severity-coded output, collected daily reports. OpenBSD built simplicity: three scripts, three local overrides, security baked into the daily routine. Linux built a general-purpose process management system and asked it to also handle cron jobs.

All three work. The BSDs understood, decades ago, that the problem was never scheduling. It was accountability. One configuration file. One email. One morning coffee. Rather civilised, really.

cron tells you when. periodic tells you what happened. FreeBSD: one config file, severity-coded output, daily email report. OpenBSD: pledge/unveil violation reports every morning. Linux: twenty files for ten tasks. The BSDs understood that the problem was never scheduling. It was accountability.