How to Set Up Cron Jobs and Periodic Tasks on FreeBSD
Automating repetitive tasks is fundamental to maintaining a healthy FreeBSD server. Whether you need to run nightly backups, scrub ZFS pools, rotate logs, or renew TLS certificates, FreeBSD gives you two complementary mechanisms: the classic cron daemon and the BSD-specific periodic framework.
This guide covers both in depth. By the end you will know how to write crontab entries, manage the system crontab, configure periodic.conf, build your own periodic scripts, and follow best practices for logging, error handling, and security.
Task Scheduling on FreeBSD: Cron and the Periodic Framework
Every Unix-like system ships with cron, but FreeBSD adds a layer on top called periodic. Understanding how the two relate is the first step.
Cron is the low-level scheduler. It reads crontab files and executes commands at the times you specify. FreeBSD uses the Vixie cron implementation, which supports per-user crontabs, a system-wide crontab, and the /etc/cron.d/ directory for drop-in files.
Periodic is a shell-based framework unique to BSD systems. It organizes administrative tasks into three categories -- daily, weekly, and monthly -- and provides a single configuration file (/etc/periodic.conf) to enable, disable, or tune every task. Cron triggers periodic at the appropriate intervals; periodic then runs each enabled script in order.
The default FreeBSD /etc/crontab already contains entries that invoke periodic:
shell1 3 * * * root periodic daily 15 4 * * 6 root periodic weekly 30 5 1 * * root periodic monthly
So cron is the engine, and periodic is a management layer that keeps system maintenance organized and configurable.
Crontab Fundamentals
A crontab line has five time-and-date fields followed by a command:
shell# minute hour day-of-month month day-of-week command 0 2 * * * /usr/local/bin/backup.sh
The Five Fields
| Field | Allowed Values | Description |
|-------|---------------|-------------|
| Minute | 0-59 | Minute of the hour |
| Hour | 0-23 | Hour of the day (24-hour clock) |
| Day of month | 1-31 | Day of the month |
| Month | 1-12 or jan-dec | Month of the year |
| Day of week | 0-7 or sun-sat | Day of the week (0 and 7 are both Sunday) |
Operators
- Asterisk (
*) -- matches every value. - Comma (
,) -- list of values:1,15means the 1st and 15th. - Hyphen (
-) -- range:1-5means Monday through Friday. - Slash (
/) -- step:*/10means every 10 units.
Special Strings
FreeBSD's cron supports shorthand strings in place of the five fields:
| String | Equivalent |
|--------|-----------|
| @reboot | Run once at startup |
| @yearly or @annually | 0 0 1 1 * |
| @monthly | 0 0 1 |
| @weekly | 0 0 0 |
| @daily or @midnight | 0 0 * |
| @hourly | 0 |
Example using a special string:
shell@reboot /usr/local/bin/start-my-service.sh
User Crontabs
Every user on the system can have a personal crontab. These files are stored in /var/cron/tabs/ and should never be edited directly. Use the crontab command instead.
Editing Your Crontab
shcrontab -e
This opens your crontab in the editor defined by EDITOR or VISUAL (defaults to vi). Save and exit to install the new crontab. Cron picks up the changes automatically -- no restart needed.
Listing Your Crontab
shcrontab -l
Removing Your Crontab
shcrontab -r
Be careful: this deletes the entire crontab without confirmation.
Managing Another User's Crontab
As root you can manage any user's crontab:
shcrontab -u www -e # edit the www user's crontab crontab -u www -l # list it
User Crontab Format
User crontabs do not include a username field. Each line is simply:
shellminute hour dom month dow command
Example -- run a script every day at 3:30 AM:
shell30 3 * * * /home/deploy/scripts/cleanup.sh
System Crontab
The system-wide crontab lives at /etc/crontab. Unlike user crontabs, it includes an extra field for the username the command should run as:
shellminute hour dom month dow who command
Here is the default FreeBSD /etc/crontab (slightly simplified):
shellSHELL=/bin/sh PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin # # minute hour mday month wday who command # */11 * * * * operator /usr/libexec/save-entropy 1 3 * * * root periodic daily 15 4 * * 6 root periodic weekly 30 5 1 * * root periodic monthly 1,31 0-5 * * * root adjkerntz -a
Key Differences from User Crontabs
- Username field -- the sixth field specifies which user runs the command.
- Environment variables -- you can set
SHELL,PATH,MAILTO, and other variables at the top. - Edited directly -- use your text editor on
/etc/crontab. Do not usecrontab -efor the system crontab. - Drop-in files -- FreeBSD also reads files from
/etc/cron.d/, which follow the same format. Packages can drop scheduled tasks there without modifying/etc/crontab.
The Periodic Framework Explained
The periodic framework lives in /etc/periodic/ and /usr/local/etc/periodic/. Each directory contains subdirectories:
shell/etc/periodic/ daily/ weekly/ monthly/ security/ /usr/local/etc/periodic/ daily/ weekly/ monthly/ security/
When cron runs periodic daily, the periodic utility:
- Reads
/etc/defaults/periodic.conffor default settings. - Reads
/etc/periodic.conffor your overrides. - Executes every enabled script in
/etc/periodic/daily/and/usr/local/etc/periodic/daily/in numeric order. - Collects output and sends it according to
daily_outputsettings.
Scripts are numbered (e.g., 100.clean-disks, 400.status-disks) so they run in a predictable order.
Built-in Periodic Tasks
FreeBSD ships with a rich set of maintenance scripts out of the box.
Daily Tasks
| Script | Purpose |
|--------|---------|
| 100.clean-disks | Remove old files from /tmp and other scratch directories |
| 110.clean-tmps | Clean world-readable temp directories |
| 120.clean-preserve | Clean /var/preserve |
| 200.backup-passwd | Back up /etc/master.passwd and /etc/group |
| 210.backup-aliases | Back up /etc/mail/aliases |
| 400.status-disks | Report disk status via df |
| 450.status-security | Run security checks |
| 999.local | Run /etc/daily.local if it exists |
Weekly Tasks
| Script | Purpose |
|--------|---------|
| 310.locate | Rebuild the locate database |
| 320.whatis | Rebuild the whatis database |
| 340.noid | Find files not owned by any user or group |
| 450.status-security | Weekly security report |
| 999.local | Run /etc/weekly.local if it exists |
Monthly Tasks
| Script | Purpose |
|--------|---------|
| 200.accounting | Process login accounting data |
| 450.status-security | Monthly security report |
| 999.local | Run /etc/monthly.local if it exists |
These tasks generate the daily/weekly/monthly status emails that FreeBSD administrators are familiar with.
Customizing periodic.conf
The default values live in /etc/defaults/periodic.conf. Never edit that file. Instead, create or edit /etc/periodic.conf with your overrides.
Controlling Output
sh# Send daily output to root via email daily_output="root" # Send weekly output to a specific address weekly_output="admin@example.com" # Log monthly output to a file instead of email monthly_output="/var/log/monthly.log" # Suppress output entirely daily_output="/dev/null"
Enabling and Disabling Tasks
Each built-in script has a corresponding variable:
sh# Disable cleaning of /tmp (if you manage it differently) daily_clean_tmps_enable="NO" # Enable the locate database rebuild weekly_locate_enable="YES" # Disable login accounting monthly_accounting_enable="NO"
Security Reports
The security subsystem runs as part of the daily tasks by default:
sh# Enable security checks daily_status_security_enable="YES" # Enable specific checks daily_status_security_pkgaudit_enable="YES" daily_status_security_tcpwrap_enable="YES"
Package Audit
FreeBSD can automatically check for known vulnerabilities in installed packages:
sh# In /etc/periodic.conf daily_status_security_pkgaudit_enable="YES"
This runs pkg audit -F daily, updating the vulnerability database and reporting any installed packages with known issues. See our FreeBSD update guide for more on keeping your system current.
Writing Custom Periodic Scripts
You can add your own scripts to the periodic framework. Place them in /usr/local/etc/periodic/daily/, /usr/local/etc/periodic/weekly/, or /usr/local/etc/periodic/monthly/.
A periodic script must follow a specific structure:
sh#!/bin/sh # If there is a global system configuration file, suck it in. if [ -r /etc/defaults/periodic.conf ]; then . /etc/defaults/periodic.conf source_periodic_confs fi case "$daily_custom_zfsscrub_enable" in [Yy][Ee][Ss]) echo "" echo "Starting ZFS scrub on zroot:" /sbin/zpool scrub zroot rc=$? if [ $rc -eq 0 ]; then echo "ZFS scrub initiated successfully." else echo "ZFS scrub failed with exit code $rc." fi ;; *) rc=0 ;; esac exit $rc
Save this as /usr/local/etc/periodic/daily/800.zfs-scrub and make it executable:
shchmod 755 /usr/local/etc/periodic/daily/800.zfs-scrub
Then enable it in /etc/periodic.conf:
shdaily_custom_zfsscrub_enable="YES"
Script Conventions
- Source
periodic.confat the top so your enable variable works. - Use a
casestatement to check your enable variable. - Print output -- periodic collects stdout and includes it in the report.
- Exit with a meaningful code -- 0 for success, nonzero for failure.
- Number your script to control execution order (100-999).
For more on ZFS maintenance tasks, see our ZFS guide.
Practical Examples
ZFS Pool Scrub via Cron
If you prefer a simple cron entry over a periodic script:
shell# Scrub the zroot pool every Sunday at 2 AM 0 2 * * 0 root /sbin/zpool scrub zroot
Automated Backups
Schedule a nightly backup using a custom script:
shell# Run backup at 1:30 AM every day 30 1 * * * root /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
A minimal backup script might use zfs send for snapshot-based backups:
sh#!/bin/sh DATASET="zroot/data" SNAP="${DATASET}@backup-$(date +\%Y\%m\%d)" /sbin/zfs snapshot "$SNAP" /sbin/zfs send "$SNAP" | /usr/bin/gzip > /backup/data-$(date +%Y%m%d).zfs.gz # Clean up snapshots older than 7 days /sbin/zfs list -t snapshot -o name -H | grep "^${DATASET}@backup-" | \ head -n -7 | xargs -n 1 /sbin/zfs destroy
For a deeper dive into backup strategies, see our FreeBSD backup guide.
Log Cleanup
Remove application logs older than 30 days:
shell# Clean old logs every day at 4 AM 0 4 * * * root /usr/bin/find /var/log/myapp -name "*.log" -mtime +30 -delete
TLS Certificate Renewal
If you use acme.sh or certbot for Let's Encrypt certificates:
shell# Attempt renewal twice daily (as recommended by Let's Encrypt) 0 0,12 * * * root /usr/local/bin/certbot renew --quiet --deploy-hook "/usr/sbin/service nginx reload"
Or with acme.sh:
shell0 0 * * * root /root/.acme.sh/acme.sh --cron --home /root/.acme.sh >> /var/log/acme.log 2>&1
Package Audit via Cron
Run a standalone vulnerability check and email results:
shell0 6 * * * root /usr/sbin/pkg audit -F 2>&1 | mail -s "pkg audit report" admin@example.com
Database Maintenance
Vacuum a PostgreSQL database weekly:
shell0 3 * * 0 postgres /usr/local/bin/vacuumdb --all --analyze --quiet
System Update Check
Check for FreeBSD security patches daily:
shell0 5 * * * root /usr/sbin/freebsd-update cron
Note the use of freebsd-update cron rather than freebsd-update fetch -- the cron subcommand adds a random delay to avoid thundering-herd problems on the update servers.
Error Handling and Logging
MAILTO
Set the MAILTO variable at the top of a crontab to receive email when any job produces output:
shellMAILTO=admin@example.com SHELL=/bin/sh PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin 0 2 * * * /usr/local/bin/backup.sh
Cron sends email whenever a job writes to stdout or stderr. To suppress email for a specific job, redirect its output:
shell0 2 * * * /usr/local/bin/backup.sh > /dev/null 2>&1
To send output to a log file instead:
shell0 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
Logging to Syslog
For centralized logging, pipe cron job output through logger:
shell0 2 * * * /usr/local/bin/backup.sh 2>&1 | /usr/bin/logger -t backup-cron
This writes output to syslog with the tag backup-cron, which you can then filter in /etc/syslog.conf or /usr/local/etc/rsyslog.conf.
Exit Codes in Scripts
Always use meaningful exit codes in your scripts:
sh#!/bin/sh set -e if ! /sbin/zpool scrub zroot; then echo "ERROR: ZFS scrub failed" >&2 exit 1 fi echo "ZFS scrub completed successfully" exit 0
Monitoring Cron Job Success and Failure
Knowing a job was scheduled is not the same as knowing it ran successfully. Here are practical approaches to monitoring.
Check Cron Logs
Cron logs job execution to syslog. On FreeBSD the relevant entries appear in /var/log/cron:
shgrep backup /var/log/cron
This shows when the job started but does not indicate success or failure of the command itself.
Heartbeat Monitoring
For critical jobs, use a dead man's switch service. At the end of your script, ping a monitoring URL:
sh#!/bin/sh /usr/local/bin/backup.sh && /usr/local/bin/curl -fsS --retry 3 https://monitor.example.com/ping/your-uuid > /dev/null
If the ping does not arrive within the expected window, the monitoring service alerts you.
Timestamp File
A simpler approach: write a timestamp file on success, and have a separate monitoring job check its age:
sh# In backup.sh (at the end, on success) date > /var/run/backup.lastrun
shell# Cron job to check if backup ran in the last 25 hours 0 8 * * * root /usr/bin/find /var/run -name backup.lastrun -mtime +1 -exec echo "WARNING: backup did not run" \;
Periodic Output Review
Periodic collects all output and delivers it according to your daily_output setting. Read these reports. If a script fails, its error output appears in the daily email or log file.
Security Considerations
Cron jobs run with elevated privileges more often than not. Treat them with the same care you would give any privileged code.
Set PATH Explicitly
Never rely on the inherited PATH. Set it at the top of your crontab or inside each script:
shPATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin
This prevents path-injection attacks where a malicious binary in an unexpected directory gets executed instead of the intended command.
Use Absolute Paths
In scripts called by cron, use absolute paths for every command:
sh/sbin/zpool scrub zroot # good zpool scrub zroot # risky
File Permissions
Cron scripts should be owned by root (or the user running them) and not writable by others:
shchown root:wheel /usr/local/bin/backup.sh chmod 750 /usr/local/bin/backup.sh
If a non-root user can write to a script that cron runs as root, that user effectively has root access.
Restrict crontab Access
Control who can use crontab with /var/cron/allow and /var/cron/deny:
- If
/var/cron/allowexists, only users listed in it can use crontab. - If
/var/cron/allowdoes not exist but/var/cron/denydoes, everyone except listed users can use crontab. - If neither file exists, only root can use crontab (default FreeBSD behavior allows all users -- check your policy).
sh# Only allow root and the deploy user to use crontab echo "root" > /var/cron/allow echo "deploy" >> /var/cron/allow
Environment Variables
Cron jobs run with a minimal environment. Do not assume variables like HOME, LANG, or USER are set. If your script needs them, define them explicitly:
sh#!/bin/sh export HOME=/root export LANG=en_US.UTF-8 export PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin
Avoid Storing Secrets in Crontabs
If a job needs credentials (database passwords, API keys), store them in a file readable only by the user running the job:
shchown root:wheel /usr/local/etc/backup.conf chmod 600 /usr/local/etc/backup.conf
Then source or read the file inside the script:
sh. /usr/local/etc/backup.conf
Never put passwords directly in crontab lines -- they are visible to anyone who can read the crontab.
Troubleshooting Common Issues
Job Does Not Run
- Check syntax -- run
crontab -land verify the time fields. - Check the cron log -- look at
/var/log/cronfor errors. - Check that the cron daemon is running --
service cron status. - Check permissions -- is the script executable? Does the user have crontab access?
Job Runs but Produces No Output
- Check output redirection -- if you redirect to
/dev/null, you will not see errors. - Check MAILTO -- is it set? Is mail delivery working?
- Test the command manually -- run it as the same user cron would use:
shsu -m www -c '/usr/local/bin/myscript.sh'
Job Runs at the Wrong Time
- Check the timezone -- cron uses the system timezone by default. Set
CRON_TZin the crontab if needed (FreeBSD 14+). - Day-of-month and day-of-week interaction -- if you specify both, cron runs the job when either condition is met (this is an OR, not an AND). This catches many people off guard.
Periodic Script Does Not Execute
- Check the enable variable -- is
daily_yourscript_enable="YES"set in/etc/periodic.conf? - Check the script is executable --
chmod 755. - Run periodic manually --
periodic dailyas root and look for output.
Frequently Asked Questions
How do I run a cron job every 5 minutes?
Use the step operator:
shell*/5 * * * * /usr/local/bin/check-service.sh
Can I run a cron job at a random time within a window?
Yes. Use sleep with a random delay. For example, to run sometime within a 60-minute window starting at 2 AM:
shell0 2 * * * root sleep $(jot -r 1 0 3600) && /usr/local/bin/task.sh
jot -r 1 0 3600 generates a random number between 0 and 3600 (seconds).
What is the difference between /etc/crontab and crontab -e?
/etc/crontab is the system-wide crontab. It includes a username field and is edited directly with a text editor. crontab -e edits a per-user crontab stored in /var/cron/tabs/ and does not have a username field. Use /etc/crontab or /etc/cron.d/ for system tasks; use crontab -e for user-specific tasks.
How do I see all scheduled cron jobs on the system?
There is no single command. You need to check multiple locations:
sh# System crontab cat /etc/crontab # Drop-in crontabs ls /etc/cron.d/ # Each user's crontab (as root) for user in $(cut -d: -f1 /etc/passwd); do crontab_content=$(crontab -u "$user" -l 2>/dev/null) if [ -n "$crontab_content" ]; then echo "=== $user ===" echo "$crontab_content" fi done
Should I use cron or periodic for system maintenance?
Use periodic for standard system maintenance tasks -- it provides a clean configuration interface, organized output, and integrates with FreeBSD's existing daily/weekly/monthly reporting. Use plain cron for application-specific tasks, jobs that need to run more frequently than daily, or jobs that do not fit the periodic model.
How do I prevent a cron job from running if the previous instance is still running?
Use a lock file. FreeBSD includes lockf(1) for exactly this purpose:
shell*/5 * * * * root /usr/bin/lockf -s -t 0 /var/run/myjob.lock /usr/local/bin/myjob.sh
The -s flag silences the error if the lock cannot be acquired, and -t 0 means do not wait -- fail immediately if the lock is held.
How do I temporarily disable all cron jobs?
Stop the cron daemon:
shservice cron stop
Or, to disable it across reboots:
shsysrc cron_enable="NO" service cron stop
To re-enable:
shsysrc cron_enable="YES" service cron start
Conclusion
FreeBSD gives you both a solid cron implementation and the periodic framework -- together they cover everything from running a command every five minutes to managing complex daily system maintenance routines. Use cron for granular scheduling and periodic for organized system administration tasks.
Set PATH explicitly, use absolute paths, lock concurrent jobs with lockf, and monitor your jobs through logs, email, or heartbeat services. A well-maintained crontab and a properly configured periodic.conf are the backbone of a reliable FreeBSD server.