There is no universal cron jobs log. The cron daemon can record that it tried to launch a command, usually through the syslog LOG_CRON facility or the systemd journal, but the command's stdout and stderr are separate. Unredirected output may be mailed, redirected output goes wherever the crontab specifies, and neither proves that the job achieved its business result.
Troubleshoot those evidence layers in order: verify the scheduler service, find the launch record for a bounded time window, inspect the correct crontab, capture stdout and stderr, preserve the exit code, and validate the expected artifact or checkpoint. This separates “cron never launched it” from “the command failed” and “the command exited zero but produced stale or incomplete work.”
Find the cron daemon and its launch records
First determine which service name and cron implementation the host uses. On systemd hosts, these commands are read-only; one or both units may not exist:
systemctl status cron.service --no-pager
systemctl status crond.service --no-pager
Debian-family packages commonly use cron.service. Cronie commonly installs crond.service. A missing unit is not evidence that scheduling is absent: the host may use another implementation, a container-specific supervisor, or systemd timers instead. Check installed packages and the host's approved service inventory before changing anything.
Once the unit is known, query a narrow time range. journalctl --unit, --since, and --until filter at the journal rather than sending every system message through grep (journalctl manual):
sudo journalctl -u cron.service \
--since "2026-09-16 01:50:00" \
--until "2026-09-16 02:20:00" \
--no-pager
sudo journalctl -u crond.service \
--since "2026-09-16 01:50:00" \
--until "2026-09-16 02:20:00" \
--no-pager
System-journal access depends on local permissions. Use sudo only with authority. The timestamps above are illustrative; use the host's time zone and the smallest interval that covers the expected run.
Cron daemon messages commonly go through the syslog cron facility, but the logging configuration decides the destination. /var/log/syslog is common on Debian and Ubuntu only when rsyslog or another logger routes cron records there. /var/log/cron is common in RHEL-family or Cronie configurations, but it is not guaranteed. Debian documents that its cron daemon logs actions to the syslog cron facility, while Cronie can also direct job output to syslog when started with its implementation-specific option (Debian cron(8), Cronie cron(8)).
If the host uses files, verify the local rsyslog or journald routing first, then search the expected command or user rather than scanning every cron line:
sudo grep -F '/usr/local/libexec/run-nightly-export' /var/log/syslog
sudo grep -F '/usr/local/libexec/run-nightly-export' /var/log/cron
A launch line proves only that the daemon invoked a shell command. Its absence can mean the wrong service, wrong host, wrong time range, logging disabled or expired, an unreadable crontab, or a schedule that did not match. Its presence does not prove the command completed.
Inspect the crontab that actually owns the job
List a user's installed table through crontab; do not read or edit spool files directly:
crontab -l
sudo crontab -u appuser -l
The first command lists the current user's table. The second is also read-only, but requires authority to inspect appuser's table. Crontabs can expose paths, account names, or badly placed credentials, so do not paste their full contents into tickets or chat.
A user crontab has five schedule fields followed by the command:
17 2 * * * /usr/local/libexec/run-nightly-export
/etc/crontab and files under /etc/cron.d commonly add a username between the five schedule fields and the command:
17 2 * * * appuser /usr/local/libexec/run-nightly-export
That extra field is a frequent source of mistakes when an entry is copied between table types. Ownership, file mode, filename, symlink, newline, and regular-file rules vary by implementation and distribution. Debian and Cronie document different details, so verify the installed manual rather than applying one distribution's rule everywhere (Debian crontab(5), Cronie crontab(5)).
For a known system table, a bounded read is safer than dumping every scheduled command:
sudo sed -n '1,120p' /etc/cron.d/nightly-export
Do not edit a table until its owner, source, and deployment method are known. Package-managed files and configuration-managed files can overwrite a manual correction.
Check schedule, time, and host availability
Confirm the host clock, time zone, and expected schedule together. Also check whether the host was running at the scheduled minute. Traditional cron evaluates matching times while its daemon runs; it is not generally a missed-run queue. Anacron and systemd timers can provide catch-up behavior, but only when explicitly configured.
Daylight-saving behavior is not universal. The current Cronie crontab manual describes missing local times as not matching and repeated local times as matching twice, while Cronie's daemon manual and Debian's patched cron document special handling for clock changes under three hours. Debian runs fixed-time jobs skipped by a forward change soon afterward and avoids rerunning them during a backward change. Identify the installed daemon and test its documented behavior before relying on a wall-clock schedule near a transition.
Some implementations support a per-table timezone such as Cronie's CRON_TZ; Debian's cron documents one daemon timezone and says setting TZ affects the child command, not when cron launches it. Use UTC when it matches the business requirement, or document the intended local-time behavior explicitly.
Two syntax details cause subtle extra or missing runs:
- In common cron implementations and POSIX crontab, an unescaped
%in the command becomes a newline, and input after the first one is passed to the command's stdin. Escape literal percent signs used by commands such asdatein a crontab command, or put complex logic in a script. - When both day-of-month and day-of-week are restricted, common cron semantics match when either field matches.
30 4 1 * 5runs at 04:30 on the first of the month and every Friday, not only when the first is Friday (POSIX crontab).
Keep the crontab small. Put conditional date logic, validation, and logging in a versioned script that can be reviewed and tested.
Reproduce the cron environment safely
Cron does not necessarily run with the same environment, working directory, interactive shell, or profile files as a login session. However, saying it has “no environment” is incorrect. POSIX requires default HOME, LOGNAME, PATH, and SHELL. Cronie sets SHELL=/bin/sh and derives HOME and LOGNAME from the account database; its path behavior can also depend on daemon options (POSIX crontab, Cronie crontab(5)).
Use absolute executable and file paths. Set a deliberate working directory in the wrapper, and define only the variables the job needs. Do not source an interactive profile as a shortcut: profiles can print output, depend on a terminal, alter PATH, or run unrelated commands.
Manual execution can change data. Do not replay a backup, billing, notification, cleanup, or import job until a dry-run mode or idempotency contract is established. In a safe environment, reproduce the important boundary as the same account with a minimal environment and explicit directory:
sudo -u appuser env -i \
HOME=/home/appuser \
LOGNAME=appuser \
PATH=/usr/local/bin:/usr/bin:/bin \
SHELL=/bin/sh \
sh -c 'cd /srv/nightly-export && /usr/local/libexec/run-nightly-export'
This is an execution, not a read-only diagnostic. Confirm authorization, inputs, external effects, credentials, and target environment first. Avoid putting secrets directly in env, command arguments, or the crontab because process listings, logs, mail, and shell history may expose them. Use the application's approved credential mechanism and least-privilege account.
When a job works interactively but not under cron, compare these facts:
- exact user and group membership;
PATH, locale, timezone, and application-specific variables;- working directory and relative paths;
- file, directory, mount, network, and credential access;
- shell syntax and executable interpreter;
- resource limits, security policy, and service availability at run time.
Fix the specific difference. Do not use chmod 777, recursive ownership changes, or root execution to conceal a missing permission.
Interpret each evidence gap
The first missing or contradictory record identifies the next boundary to inspect:
- No active service and no launch record: confirm the correct scheduler, host, and deployment. Do not start or enable a unit until the intended scheduler is known; doing so can create duplicate runs.
- Service active but no launch record: inspect the correct user's installed table, the exact system table, the schedule, host uptime, time zone, and implementation-specific file rules. Check whether retained logs cover the expected time.
- Launch record but no job output: verify mail and redirection. The command may be silent, may have failed before opening its log, or may write elsewhere. Confirm the wrapper path and the job user's ability to traverse each parent directory and open the target file.
- Output stops without an end event: look for termination, timeout, resource pressure, host shutdown, or a blocked dependency. A start event alone does not prove the process is still running. Check process and application state with approved diagnostics.
- Nonzero exit: preserve the original code and bounded stderr message. Interpret the code from that program's current documentation rather than assigning a generic meaning.
- Zero exit but wrong data: inspect the run-specific checkpoint, output freshness, expected count, destination, and high-water mark. This is a business-validation failure even though the command reported success.
- More than one run: compare run IDs, cron sources, hosts, retries, and lock evidence. Duplicates can come from two tables, two schedulers, a retry wrapper, or a cluster running the same local table.
Avoid changing several boundaries at once. A schedule edit, permission change, environment change, and manual replay in one step destroys the evidence needed to identify the actual cause.
Capture stdout and stderr without weakening permissions
If a command does not redirect output, POSIX leaves delivery to an implementation-defined mail mechanism. Cronie uses MAILTO; a non-empty value selects a recipient, and MAILTO="" disables mail. This is useful only when a functioning mail transport and monitored destination exist (Cronie crontab(5)).
For file logging, pre-create a dedicated directory and file with the narrowest practical ownership. These commands change system state and require approval. The second command creates a missing file under a restrictive umask without truncating an existing file; inspect an existing path's type, owner, mode, and symlink target separately before relying on it:
sudo install -d -o appuser -g appuser -m 0750 /var/log/acme-jobs
sudo -u appuser sh -c 'umask 027; : >> /var/log/acme-jobs/nightly-export.log'
Then use an explicit shell, PATH, umask, wrapper, and append redirection:
SHELL=/bin/sh
PATH=/usr/local/bin:/usr/bin:/bin
MAILTO=""
17 2 * * * umask 027; /usr/local/libexec/run-nightly-export >> /var/log/acme-jobs/nightly-export.log 2>&1
>> file opens stdout in append mode. The following 2>&1 makes stderr point to that already-redirected stdout. Reversing them as 2>&1 >> file leaves stderr attached to its earlier destination and does not combine the streams as intended.
Output can contain tokens, connection strings, customer data, filenames, or command arguments. Keep diagnostic output bounded, redact at the source, restrict log access, and set a retention policy. Do not enable shell tracing with secrets in scope.
Rotating the file prevents indefinite disk growth. This illustrative logrotate policy is state-changing and must be adapted to the required retention and local account names:
/var/log/acme-jobs/nightly-export.log {
daily
rotate 14
compress
delaycompress
su appuser appuser
missingok
notifempty
create 0640 appuser appuser
}
logrotate supports time- and size-based rotation, compression, removal, and explicit file creation (logrotate manual). There is no universal retention count. Choose it from recovery, audit, privacy, capacity, and legal needs. Because each cron run reopens this file, rename-and-create rotation is usually simpler than copytruncate; a job already writing during rotation can continue on the renamed inode until it closes the descriptor.
delaycompress leaves the newest rotated file uncompressed for one cycle so a process that still has it open can finish writing. If a job can remain open beyond a rotation cycle, coordinate rotation with the job instead of assuming this example prevents loss.
Record start, exit status, duration, and business outcome
The wrapper below uses POSIX shell syntax on Linux. It assumes the local date supports +%s, as GNU coreutils does. It logs only controlled fields, captures the command's exit status immediately, and checks a run-specific completion file. Replace the command and checkpoint contract with the real job's documented, read-only proof of success.
#!/bin/sh
set -u
umask 027
job='nightly_export'
run_id="$(date -u '+%Y%m%dT%H%M%SZ').$$"
start_epoch="$(date +%s)"
checkpoint="/var/lib/acme-jobs/nightly-export/${run_id}.complete"
emit() {
level=$1
event=$2
command_exit=$3
outcome=$4
duration=$5
printf 'timestamp=%s job=%s run_id=%s level=%s event=%s command_exit=%s outcome=%s duration_seconds=%s\n' \
"$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$job" "$run_id" "$level" \
"$event" "$command_exit" "$outcome" "$duration"
}
emit INFO start - running 0
RUN_ID="$run_id" COMPLETION_FILE="$checkpoint" \
/usr/local/bin/nightly-export
command_exit=$?
end_epoch="$(date +%s)"
duration=$((end_epoch - start_epoch))
if [ "$command_exit" -ne 0 ]; then
emit ERROR end "$command_exit" technical_failure "$duration"
exit "$command_exit"
fi
if [ ! -s "$checkpoint" ]; then
emit ERROR end 0 business_check_failed "$duration"
exit 3
fi
emit INFO end 0 success "$duration"
exit 0
The sample run ID, paths, and events are illustrative. The command receives a run ID and expected completion-file path without logging other arguments or environment values. Its raw exit code remains in command_exit. A nonzero command code is returned unchanged; a zero command followed by a failed business check returns the wrapper-specific code 3.
An exit code of zero means only that the program reported technical success. The output may still be empty, incomplete, stale, duplicated, or written to the wrong destination. Validate the result that matters: a run-specific checkpoint, artifact checksum, source-to-destination count, last processed sequence, or application status. Make the check specific enough that an old file cannot satisfy a new run.
The wrapper also provides evidence for long runtime and clock problems, but wall-clock duration can be distorted by a clock adjustment. Use a monotonic timer inside applications that require precise duration. Set any timeout according to the job's safe cancellation and recovery behavior; killing a database migration or upload at an arbitrary duration can leave partial work.
Prevent overlapping runs without hiding contention
An interval shorter than the job's runtime can create duplicates, contention, and out-of-order writes. The util-linux flock command can place an advisory lock around a command. In nonblocking mode it returns immediately when the lock is held, and -E selects a distinct conflict exit code (flock manual).
Use it only after confirming the filesystem implements the required locking semantics. The manual notes limitations on some NFS and CIFS configurations. Pre-create a lock directory writable only by the job account; do not place a shared writable lock in an untrusted directory.
This small wrapper makes contention visible in the redirected job log:
#!/bin/sh
lock='/var/lib/acme-jobs/locks/nightly-export.lock'
flock -n -E 75 "$lock" /usr/local/libexec/run-nightly-export
status=$?
if [ "$status" -eq 75 ]; then
printf 'timestamp=%s job=nightly_export level=WARNING event=lock_contended exit_code=75\n' \
"$(date -u '+%Y-%m-%dT%H:%M:%SZ')" >&2
fi
exit "$status"
Reserve exit code 75 for lock contention in this wrapper; flock otherwise returns the child command's status, so a child that also uses 75 would be ambiguous. Do not append || true; that turns a skipped run into apparent success. Decide whether contention is expected coalescing, a warning, or a failed service objective, then report accordingly. flock is an execution guard, not proof that the previous run completed correctly.
Detect missed and incorrect runs
A keyword alert can detect a failure event that exists. It cannot detect silence by itself. Missed-run detection needs a completion heartbeat or checkpoint plus a separate monitor that explicitly supports absence, freshness, or dead-man checks.
Define the contract for each important job:
- earliest and latest acceptable completion time;
- unique run or schedule identifier;
- command exit and business-valid outcome;
- expected artifact, count, checkpoint, or high-water mark;
- maximum runtime and overlap policy;
- duplicate and retry semantics;
- owner, escalation path, and safe first diagnostic;
- clock and timezone used for the deadline.
The absence monitor should evaluate the completion signal after the allowed window, not merely search for an error string. Account for delayed log delivery and clock skew. A late heartbeat must not silently rewrite an earlier missed-run incident as if it never happened.
Test the complete path with a harmless job or dry-run fixture. Verify a normal run, nonzero exit, failed business check, missing permission, missing environment value, long runtime, lock contention, host downtime, log rotation, and delayed or dropped collection. Do not replay production work to test observability.
Use centralized logs after local evidence works
Local cron, stdout, stderr, exit, and checkpoint evidence remain the source of truth. Once daemon or job logs are already delivered through a supported collector or receiver, a centralized log service can make cross-host investigation and retention easier.
Fluxtail log management is available through paid Starter and Pro plans and is logs-focused. It can search and filter received events, show recent rows in Live Tail, and evaluate log alerts when the needed cron fields are actually delivered and mapped. It does not schedule jobs, track exit codes automatically, parse cron output automatically, test cron health, or infer a missing run from the absence of an event.
Send a unique harmless marker through the configured collector and confirm the retained row before relying on a search or alert. Keep the host's local evidence available during collector or network outages. Log management best practices explains the broader retention and collection controls, while alerting best practices covers ownership, missing-data behavior, and safe response boundaries.
Cron log troubleshooting checklist
Work through the evidence in this order:
- Identify the host, expected time zone, cron implementation, and service unit.
- Confirm the service state without changing it.
- Query the unit and exact time window for a launch record.
- List the correct user's table or inspect the exact system table with authority.
- Check five-field versus username-field syntax,
%, day matching, timezone, DST behavior, and host uptime. - Confirm user, working directory,
PATH, permissions, credentials, mounts, and dependencies. - Capture stdout and stderr into a protected, rotated destination.
- Log a bounded run ID, start, end, raw command exit, duration, and business outcome.
- Verify the run-specific artifact or checkpoint; never equate exit zero with correct work.
- Make overlap and lock contention visible.
- Monitor completion freshness with an absence-capable system, not a keyword-only alert.
- Test collection failures and keep sensitive output out of logs, mail, and command arguments.
This sequence narrows the fault without guessing: scheduler, table, schedule, environment, command, result, or monitoring. Fix the first failed boundary and verify the original job outcome before changing another variable.