Skip to content
FreeBSDDeep Dive Published Updated 10 min readViews unavailable

FreeBSD cron Operations: Schedules, Environments, and Reliable Jobs

Run FreeBSD cron jobs predictably with correct table formats, bounded commands, overlap control, daylight-saving behavior, output routing, and checks.

The conventional five-field cron(8) schedule has minute-level granularity, while FreeBSD also documents special per-second and completion-relative forms. Cron is still a command scheduler, not a workflow engine: it decides when to launch a process, while the command remains responsible for validating inputs, handling partial failure, avoiding unsafe repetition, and reporting a useful outcome. Reliable scheduling therefore starts with three separate questions: which crontab format owns the entry, what environment the command will receive, and how an operator can prove that a scheduled run completed.

This guide focuses on application and administrative jobs, not the built-in periodic(8) maintenance framework. FreeBSD uses cron to invoke periodic groups, but custom jobs should not be inserted into periodic directories unless they are deliberately designed as periodic scripts. Keeping scheduling and maintenance frameworks distinct makes ownership and failure diagnosis clearer.

Identify the table and its execution identity

FreeBSD has two common crontab formats. A personal table installed with crontab -e has five time fields followed by a command; cron runs the command as that table’s owner. System tables such as /etc/crontab, /etc/cron.d/*, and /usr/local/etc/cron.d/* add a username between the time fields and command. The extra field is not optional in a system table, and it must not be copied into a personal crontab.

# /etc/cron.d/cache-refresh: system format, including the account
17 2 * * * appuser /usr/local/sbin/refresh-cache

The same schedule in the account’s own table omits appuser:

# installed with: crontab -e -u appuser
17 2 * * * /usr/local/sbin/refresh-cache

Prefer a dedicated, least-privilege service account for a job that does not need host administration. A system table’s root field is appropriate only when the operation genuinely needs root. Root-owned /etc/crontab is a system file and should be backed up and changed through the host’s configuration management process; a package-owned entry under /usr/local/etc/cron.d may be replaced during package maintenance, so inspect its owner and source before editing it.

For a user’s table, inspect the active configuration with crontab -l or crontab -l -u appuser; edit with crontab -e. crontab -r removes the entire table, so use it only when that removal is intentional and backed up. Cron checks the spool and system table modification times every minute and reloads changed files; a routine edit does not require restarting the daemon. Confirm the daemon state with service cron status before diagnosing individual entries.

Read the five scheduling fields precisely

The conventional fields are minute, hour, day of month, month, and day of week. FreeBSD accepts numeric lists, ranges, names for month/day, and step values. For example, */15 8-17 * * 1-5 means every fifteen minutes during the 08:00–17:59 hours on weekdays, not once every fifteen minutes from 08:00 through precisely 17:00. If both day-of-month and day-of-week are restricted rather than *, a command runs when either field matches. That OR rule is a frequent source of jobs running more often than intended.

# Weekdays at 02:17, local host time
17 2 * * 1-5 /usr/local/sbin/report --mode daily

# At 04:30 on the first and fifteenth, plus every Friday
30 4 1,15 * 5 /usr/local/sbin/report --mode period-end

Before installing a complex expression, translate it into a plain-language calendar and enumerate the next few run times with an independent schedule checker or a test table. Cron’s fields describe wall-clock times, not elapsed intervals. A job scheduled at */20 does not mean exactly twenty minutes after the previous run, and a job that lasts longer than its interval can overlap unless it has an explicit exclusion mechanism.

Special entries such as @daily and @reboot are extensions recognized by FreeBSD cron. A numeric @300 entry has a different meaning from a conventional five-field schedule: it is launched 300 seconds after cron starts or reloads the entry and then 300 seconds after each completion, so normal executions of that entry do not overlap. However, overlap can occur if the job is still running when its crontab is modified and then reloaded; a daemon restart or table reload also changes the initial reference point. Use calendar fields for calendar requirements and elapsed-after-completion scheduling only when its semantics are intended.

Make the runtime environment explicit

Cron is not an interactive login shell. FreeBSD sets SHELL, HOME, and LOGNAME, with login-class environment variables also possible; PATH has a base-system default, but that may not include every directory a local package uses. The current working directory is not guaranteed to be the script’s project directory. Interactive aliases, shell startup files, mounted filesystems, credential agents, and terminal state should not be assumed.

Set the few variables the job actually needs at the top of the table, and use absolute paths for commands and data. The following user-table example makes its shell, search path, working directory, and log destination explicit:

SHELL=/bin/sh
PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin
[email protected]
17 2 * * * cd /var/db/reporting && /usr/local/sbin/refresh-cache >> /var/log/cache-refresh.log 2>&1

The example recipient is intentionally a reserved placeholder, not a deliverable address. Configure mail routing before depending on cron mail. Output is mailed to the table owner by default; MAILTO can direct it elsewhere, and an empty MAILTO suppresses mail. Redirecting output trades mail visibility for a log-file lifecycle obligation: define rotation, retention, write permissions, and alerts for missing or growing logs. Never redirect all errors to /dev/null merely to make a successful schedule appear quiet.

Reproduce the command with a deliberately small environment and the target identity. FreeBSD’s Handbook demonstrates env -i to test cron-like conditions. Add the job’s required variables explicitly, use su -m or an equivalent controlled method to test the intended user, and record both exit status and output. A job that succeeds only in an administrator’s shell is not ready to schedule.

Treat retries, overlap, and side effects as application behavior

Cron does not provide a durable queue, transaction, exactly-once execution, or automatic retry of a failed command. If the machine is down at a scheduled minute, a conventional calendar entry is not a promise that it will be replayed after boot. A job may also complete its external change and fail before recording success, so retry logic must tolerate an ambiguous outcome.

Design scheduled work to be idempotent where possible: write to a temporary output and atomically rename it, use stable keys for remote updates, or compare the desired state before changing it. When overlap would be harmful, use an operating-system lock around the actual command rather than a fragile “lock file exists” test:

# Fail quickly instead of starting a second concurrent export
17 2 * * * root /usr/bin/lockf -t 0 /var/run/exports-refresh.lock /usr/local/sbin/refresh-exports

lockf holds the advisory lock while the command runs. The job must cooperate by using the same lock path, and a lock only coordinates processes that honor that lock; it does not protect a remote service or a second host. Verify the documented exit status and logging behavior for the installed release, then alert when the lock prevents a run if skipped executions are not acceptable. If missed jobs must be recovered, make the command inspect its last successful watermark and process all pending work rather than assuming cron will replay it.

Bound external operations with timeouts, cap output, and set an intentional working directory. Avoid a broad wildcard cleanup command unless it validates the target path, refuses unexpected values, and has a dry-run path. Ensure the script’s own error handling preserves nonzero exit codes; piping output through a logging utility without pipefail-equivalent handling can accidentally report success when the main command failed.

Account for local time and clock transitions

Cron schedules are interpreted against the host’s local time zone. FreeBSD enables cron’s special daylight-saving handling by default; jobs during a skipped or repeated local-time interval are handled differently depending on whether they are hourly or less frequent, and the daemon manual documents the exact behavior. This is not a reason to schedule irreversible one-shot business actions at an ambiguous wall-clock time. Prefer an application-level UTC schedule or a durable calendar service when a requirement means “exactly once at this absolute instant.”

When a task runs at the wrong hour, inspect the host’s configured zone and current clock before changing the cron expression. Compare date, the relevant zoneinfo data, and a verbose zdump of the zone around the transition. Do not add a second cron line to “cover both” sides of a clock transition until you have tested duplicate behavior. A job should record a run identifier and an explicit time basis (UTC timestamp or local date plus zone) so operators can distinguish two legitimate runs from a repeated wall-clock display.

Clock synchronization corrections can also change when the next minute is observed. Cron’s DST behavior is documented, but it is not a distributed event scheduler. If a task depends on strict sequencing, a single active controller, or a deadline that survives host outage, use an application or platform scheduler designed for that contract and make cron only the local trigger if needed.

Debug without turning a test into a production run

Start by confirming the table, identity, time fields, and loaded daemon. crontab -l -u appuser does not show /etc/crontab or package tables, so inspect the actual source file too. Check file syntax and ownership, then verify that the target executable exists and can be run by the configured user. Review /var/log/cron or the configured syslog destination for CMD records and correlate them with the job’s own logs.

For a controlled lab, cron’s -x debug modes include load, pars, proc, and test; test traces scheduling without performing actions. Do not launch a second production daemon with the live spool merely to inspect it. Test a copy of the table with harmless commands, or reproduce a single command directly under the intended account and environment. Confirm one expected invocation, one deliberately failing invocation, mail or log delivery, and a job that runs longer than the schedule interval.

Useful evidence includes the cron invocation record, command start/end timestamps, exit status, runtime, lock contention, bytes written, and a domain-specific success measure. A line in crontab -l proves only that configuration is installed. A cron CMD log proves only launch. A zero exit status proves only what the script defines. For a backup, for example, successful completion must include a restore test; for a report, it may require a nonempty output with the expected date range.

Set acceptance criteria before relying on a job

Before handoff, document the schedule in a human-readable sentence, the host time zone, the user, required mounts and network dependencies, maximum runtime, overlap policy, output location, alert destination, and recovery procedure for a missed run. Keep the schedule in version control or configuration management, but avoid storing credentials in the crontab. Use a package’s supported service or timer interface instead of editing package-owned files without understanding upgrade behavior.

Test the command with the exact execution account, a nearly empty environment, expected working directory, and realistic input. Test output success and failure paths and confirm alert receipt at the real destination. Observe at least one actual cron-launched run; a manual invocation does not verify parsing, environment setup, daemon state, or mail routing. For destructive or externally visible jobs, stage against a disposable target first.

After deployment, monitor last-success time rather than merely checking that cron is running. Alert on a run older than its expected cadence plus an agreed grace interval, repeated nonzero exits, excessive duration, lock contention, missing output, and an unexpected change in the host time zone. A scheduler can be healthy while every child command fails. Operational quality comes from a clear contract, tested command behavior, and verifiable outcomes, not from a syntactically valid cron line.

Related:

Sources:

Comments