FreeBSD.software
Home/Blog/How to Set Up Cron Jobs and Periodic Tasks on FreeBSD
tutorial2026-03-29

How to Set Up Cron Jobs and Periodic Tasks on FreeBSD

Complete guide to cron jobs and the periodic framework on FreeBSD. Covers crontab syntax, system crontab, periodic.conf, writing custom periodic scripts, and best practices.

# 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:

1 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:


# 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,15 means the 1st and 15th.

- **Hyphen (-)** -- range: 1-5 means Monday through Friday.

- **Slash (/)** -- step: */10 means 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:

@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

sh

crontab -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

sh

crontab -l

Removing Your Crontab

sh

crontab -r

Be careful: this deletes the entire crontab without confirmation.

Managing Another User's Crontab

As root you can manage any user's crontab:

sh

crontab -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:


minute hour dom month dow command

Example -- run a script every day at 3:30 AM:


30 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:


minute hour dom month dow who command

Here is the default FreeBSD /etc/crontab (slightly simplified):


SHELL=/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

1. **Username field** -- the sixth field specifies which user runs the command.

2. **Environment variables** -- you can set SHELL, PATH, MAILTO, and other variables at the top.

3. **Edited directly** -- use your text editor on /etc/crontab. Do not use crontab -e for the system crontab.

4. **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:

/etc/periodic/

daily/

weekly/

monthly/

security/

/usr/local/etc/periodic/

daily/

weekly/

monthly/

security/

When cron runs periodic daily, the periodic utility:

1. Reads /etc/defaults/periodic.conf for default settings.

2. Reads /etc/periodic.conf for your overrides.

3. Executes every enabled script in /etc/periodic/daily/ and /usr/local/etc/periodic/daily/ in numeric order.

4. Collects output and sends it according to daily_output settings.

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](/blog/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:

sh

chmod 755 /usr/local/etc/periodic/daily/800.zfs-scrub

Then enable it in /etc/periodic.conf:

sh

daily_custom_zfsscrub_enable="YES"

Script Conventions

1. **Source periodic.conf** at the top so your enable variable works.

2. **Use a case statement** to check your enable variable.

3. **Print output** -- periodic collects stdout and includes it in the report.

4. **Exit with a meaningful code** -- 0 for success, nonzero for failure.

5. **Number your script** to control execution order (100-999).

For more on ZFS maintenance tasks, see our [ZFS guide](/blog/zfs-freebsd-guide/).

Practical Examples

ZFS Pool Scrub via Cron

If you prefer a simple cron entry over a periodic script:

# 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:


# 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](/blog/freebsd-backup-guide/).

Log Cleanup

Remove application logs older than 30 days:


# 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:


# 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:


0 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:


0 6 * * * root /usr/sbin/pkg audit -F 2>&1 | mail -s "pkg audit report" admin@example.com

Database Maintenance

Vacuum a PostgreSQL database weekly:


0 3 * * 0 postgres /usr/local/bin/vacuumdb --all --analyze --quiet

System Update Check

Check for FreeBSD security patches daily:


0 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:


MAILTO=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:


0 2 * * * /usr/local/bin/backup.sh > /dev/null 2>&1

To send output to a log file instead:


0 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1

Logging to Syslog

For centralized logging, pipe cron job output through logger:


0 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:

sh

grep 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


# 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:

sh

PATH=/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:

sh

chown 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/allow exists, only users listed in it can use crontab.

- If /var/cron/allow does not exist but /var/cron/deny does, 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:

sh

chown 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

1. **Check syntax** -- run crontab -l and verify the time fields.

2. **Check the cron log** -- look at /var/log/cron for errors.

3. **Check that the cron daemon is running** -- service cron status.

4. **Check permissions** -- is the script executable? Does the user have crontab access?

Job Runs but Produces No Output

1. **Check output redirection** -- if you redirect to /dev/null, you will not see errors.

2. **Check MAILTO** -- is it set? Is mail delivery working?

3. **Test the command manually** -- run it as the same user cron would use:

sh

su -m www -c '/usr/local/bin/myscript.sh'

Job Runs at the Wrong Time

1. **Check the timezone** -- cron uses the system timezone by default. Set CRON_TZ in the crontab if needed (FreeBSD 14+).

2. **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

1. **Check the enable variable** -- is daily_yourscript_enable="YES" set in /etc/periodic.conf?

2. **Check the script is executable** -- chmod 755.

3. **Run periodic manually** -- periodic daily as root and look for output.

Frequently Asked Questions

How do I run a cron job every 5 minutes?

Use the step operator:

*/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:


0 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:


*/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:

sh

service cron stop

Or, to disable it across reboots:

sh

sysrc cron_enable="NO"

service cron stop

To re-enable:

sh

sysrc 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.