Cron Job Log Guide: How to Find and Read Cron Logs

Cron Job Log Guide: How to Find and Read Cron Logs

A backup job can appear healthy while producing no useful evidence at all. The scheduler may record that it launched the command, while the command itself fails under cron's restricted environment, writes output to an unexpected location, or sends diagnostics to a mail system nobody monitors. When the restore is needed, the only remaining clue may be a zero-byte dump and a log file that rotated away the evidence.

A reliable cron job log is therefore more than a place to grep for CRON. It's an operational record that connects schedule, user, command, output, exit status, duration, retention, and alerting. The practical path is to locate the correct logging backend, capture stdout and stderr explicitly, preserve rotated evidence, and monitor successful completion rather than assuming that a scheduler entry proves the work finished.

Table of Contents

When the Silent Cron Job Becomes Your Problem

A nightly database backup can run for weeks without raising an alarm. The cron daemon may write an execution line to the system log, suggesting that the schedule works. During a restore drill, the dump may be empty because the command failed, while its useful error output went nowhere.

A scheduler record confirms an attempted launch. It does not confirm that the backup completed, transferred its files, or created a valid artifact. The cron job log is the first forensic layer: it provides the execution timestamp, account, process marker, and command that an investigator can correlate with application output and the resulting files.

Cron's design explains its durability and its limits. Its history is closely tied to Unix Version 7, released in January 1979, when cron became associated with background execution of scheduled commands. Early implementations woke once per minute, read the crontab, and ran commands due at that minute. That operating model still shapes the minute-level execution records operators inspect today. The historical account of cron's development is documented in this cron history reference.

Practical rule: A CMD entry proves an attempted launch. It doesn't prove application success.

Clean tutorials often assume that logging is enabled and that every host uses a familiar file path. Production systems violate both assumptions. systemd may replace or supplement rsyslog, a minimal cloud image may have rsyslog masked, a user crontab may lack a working mail transport agent, and logrotate may already have removed the period under investigation. A missing line can indicate a stopped daemon, an unconfigured logging sink, an unreadable file, a filtered facility, or a job that was never installed.

Treat the investigation as an evidence pipeline:

  • Locate the scheduler record: Identify whether the host uses /var/log/syslog, /var/log/cron, another syslog destination, or journald.
  • Capture command output: Redirect stdout and stderr instead of relying on cron mail.
  • Reconstruct history: Search current and compressed rotated files, then correlate execution lines with application output.
  • Preserve evidence: Set an intentional rotation and retention policy for custom logs.
  • Alert on state: Detect failures, excessive duration, and missing successful heartbeats.

For a scheduled command that does not run at all, the cron jobs not running guide offers a focused troubleshooting reference. The operating requirement is straightforward: logs must let a responder determine what ran, under which account, at what time, with what result, and whether the expected work happened.

Where Cron Logs Live on Major Linux Distributions

The first diagnostic mistake is treating a distribution-specific path as a universal contract. Debian and Ubuntu commonly route cron messages through /var/log/syslog, while RHEL-family systems commonly use /var/log/cron. Some hosts expose the same facility through journald, and a dedicated /var/log/cron.log exists only when an administrator has configured it.

Start by identifying the daemon and its service state:

command -v cron || command -v crond
systemctl status cron 2>/dev/null || systemctl status crond
systemctl is-enabled cron 2>/dev/null || systemctl is-enabled crond

On Debian or Ubuntu, inspect the traditional sink:

sudo grep -i CRON /var/log/syslog | tail -50
sudo tail -f /var/log/syslog | grep --line-buffered CRON

On RHEL, CentOS, Rocky, or similar systems, use:

sudo grep -i CRON /var/log/cron | tail -50
sudo tail -f /var/log/cron

If neither file exists, check the general message log and the logging configuration:

sudo grep -i cron /var/log/messages 2>/dev/null | tail -50
sudo grep -R 'cron\.\*' /etc/rsyslog.conf /etc/rsyslog.d 2>/dev/null
systemctl status rsyslog

The presence of a rule isn't enough. A masked or stopped rsyslog service can leave the expected file empty even though cron is active. Disk pressure also matters:

df -h /var /var/log
sudo ls -l /var/log/syslog /var/log/cron /var/log/messages 2>/dev/null

Journald changes the search strategy

On systemd hosts, query the service or process directly:

sudo journalctl -u cron
sudo journalctl -u crond
sudo journalctl _COMM=cron
sudo journalctl _COMM=crond
sudo journalctl -u cron -f

If the host has rebooted since the event, determine whether journald is persistent. Inspect /etc/systemd/journald.conf for a Storage= setting, then verify available boots and disk usage:

sudo journalctl --list-boots
sudo journalctl --disk-usage

A volatile journal won't preserve older records after a restart. The operator should also check whether journald forwards messages to syslog, rather than assuming both stores contain identical data.

The practical locations and commands differ by platform:

Distribution Log File or Sink Primary Read Command Init System
Debian or Ubuntu /var/log/syslog, or journald grep CRON /var/log/syslog, journalctl -u cron systemd with rsyslog or journald
RHEL family /var/log/cron, or journald grep CRON /var/log/cron, journalctl -u crond systemd with rsyslog or journald
Arch Linux journald by default, optional syslog destination journalctl _COMM=crond systemd
Minimal systemd host journald, possibly volatile journalctl _COMM=cron or journalctl _COMM=crond systemd

The decision flow is short: confirm cron is installed and running, identify whether rsyslog or journald receives cron facility messages, verify the destination is readable, and check available disk space. If the daemon runs but no scheduler record appears, the problem is in the logging path or filtering. If a scheduler record exists but the job's own output doesn't, move to explicit stdout and stderr capture.

Capturing stdout and stderr from Cron Jobs

A cron command can launch successfully yet lose the evidence you need. Permission errors, missing binaries, authentication failures, and pipeline diagnostics may never reach a usable log. Cron can attempt to mail output, but that path depends on a correctly configured local mail transfer agent.

Use explicit append redirection as the baseline:

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

>> keeps earlier runs, and 2>&1 combines stderr with stdout. Separate the streams when operators need a normal output file and a prominent error file:

30 2 * * * /usr/local/bin/backup.sh >> /var/log/cronjobs/backup.log 2>> /var/log/cronjobs/backup-errors.log

Have the script print timestamps at meaningful milestones, or put the command behind a wrapper that records start, completion, duration, and exit status. That creates a stable evidence boundary instead of turning the crontab line into an unreadable shell program.

A wrapper creates an evidence boundary

#!/usr/bin/env bash
set -uo pipefail
set -x

job_name="${1:?job name required}"
shift
log_dir="/var/log/cronjobs/${job_name}"
mkdir -p "$log_dir"

start_epoch=$(date +%s)
started_at=$(date --iso-8601=seconds)
log_file="${log_dir}/run-$(date +%Y%m%dT%H%M%S).log"

trap 'status=$?; end_epoch=$(date +%s); finished_at=$(date --iso-8601=seconds); duration=$((end_epoch - start_epoch)); printf "[%s] job=%s status=%s duration_seconds=%s\n" "$finished_at" "$job_name" "$status" "$duration" >>"$log_file"' EXIT

printf '[%s] job=%s start\n' "$started_at" "$job_name" >>"$log_file"
"$@" >>"$log_file" 2>&1

The EXIT trap writes the completion record even if the command returns failure. The wrapper therefore preserves command output, timing, and status in one job-specific directory. The executing account must be able to create the directory and append to the log. Check those permissions before diagnosing an empty file as a cron problem.

The crontab entry stays readable:

MAILTO=""
30 2 * * * /usr/local/bin/cronrun.sh database-backup /usr/local/bin/backup.sh

set -x exposes commands as they execute, while set -u and pipefail surface unset variables and pipeline failures. Review both settings carefully. Tracing can expose credentials, and strict options can alter scripts that were written without them.

For Python commands, the Python logging to file guide covers application-side file logging. MAILTO can supplement the evidence path when local mail is configured and monitored, but it should not be the only record of a run.

Reading and Parsing Cron Job Logs Effectively

A syslog line is human-readable, but it isn't a complete job record. A typical entry contains a timestamp, hostname, process tag, process identifier, user account, and command marker:

Mar 10 02:30:01 app01 CRON[1842]: (backup) CMD (/usr/local/bin/backup.sh)

The CMD line says cron launched the command. An application log or wrapper status record is needed to establish completion and exit status. Some cron implementations also emit error text when a child fails, but the exact wording varies, so a parser shouldn't depend on one message format.

Extract the evidence that survives format changes

For classic files, begin with narrow searches:

sudo grep -h 'CRON.*CMD' /var/log/cron /var/log/syslog 2>/dev/null
sudo grep -h 'CRON.*backup.sh' /var/log/cron /var/log/syslog 2>/dev/null
sudo grep -h 'CRON.*backup.sh' /var/log/cron.1 /var/log/syslog.1 2>/dev/null
sudo zgrep -h 'CRON.*backup.sh' /var/log/cron*.gz /var/log/syslog*.gz 2>/dev/null

zgrep matters because the relevant execution may live in a compressed archive. The absence of a CMD entry around an expected time is evidence that cron didn't record a launch, but it doesn't identify the cause by itself. Check the crontab, access controls, daemon state, and time configuration before concluding that the schedule was skipped.

A simple field extraction pattern can make a line easier to ingest:

sudo awk '
/CRON/ {
  timestamp = $1 " " $2 " " $3
  host = $4
  tag = $5
  sub(/:$/, "", tag)
  print timestamp "," host "," tag "," $0
}' /var/log/cron /var/log/syslog 2>/dev/null

This is intentionally conservative. Syslog timestamps lack a year, and command text may contain spaces, shell operators, and quoted arguments. A production parser should preserve the original line rather than attempting to split the command into positional fields.

Journald offers structured metadata

Journal queries can emit JSON, which is safer for downstream processing:

sudo journalctl -u cron -o json --no-pager |
  jq -r '[
    .__REALTIME_TIMESTAMP,
    ._HOSTNAME,
    ._UID,
    ._PID,
    .PRIORITY,
    .MESSAGE
  ] | @csv'

The key distinction is between journal metadata and message content:

Field Syslog Example Journalctl JSON Key Meaning
Timestamp Mar 10 02:30:01 __REALTIME_TIMESTAMP Event time recorded by the logging system
Host app01 _HOSTNAME Machine that emitted the record
Process CRON[1842] _COMM, _PID Scheduler process identity
Account (backup) in MESSAGE _UID may identify the process user User context, depending on implementation
Event CMD (/path/job) MESSAGE Cron's textual action record
Severity Often implicit in syslog view PRIORITY Journal priority value

Duration isn't normally present in a scheduler line. It must come from wrapper output, application logs, or correlated start and finish records. Exit codes follow shell conventions, where 0 means success and a non-zero value indicates failure, but cron's own launch record may not expose that value. Operators should avoid inferring success from the absence of an error line.

A compact CSV export across current and rotated files can preserve raw evidence:

{
  printf 'source,timestamp,host,process,message\n'
  for file in /var/log/cron /var/log/cron.1 /var/log/syslog /var/log/syslog.1; do
    [ -r "$file" ] || continue
    awk -v source="$file" '
      /CRON/ {
        ts=$1 " " $2 " " $3
        host=$4
        process=$5
        sub(/:$/, "", process)
        line=$0
        gsub(/"/, "\"\"", line)
        printf "\"%s\",\"%s\",\"%s\",\"%s\",\"%s\"\n", source, ts, host, process, line
      }
    ' "$file"
  done
} > cron-evidence.csv

Log Rotation and Centralization for Cron Output

Rotation is a control, not a cleanup chore. An appended cron application log grows until the filesystem or the operator's evidence window becomes the failure. Distribution defaults vary, and a custom /var/log/cronjobs/ directory won't be protected unless a matching logrotate rule exists.

Inspect the active policy before changing it:

sudo ls -l /etc/logrotate.d
sudo grep -R -nE 'cron|syslog|messages' /etc/logrotate.conf /etc/logrotate.d 2>/dev/null
sudo logrotate -d /etc/logrotate.conf

A dedicated rule for wrapper output might look like this:

/var/log/cronjobs/*/*.log {
    daily
    rotate 14
    compress
    delaycompress
    missingok
    notifempty
    create 0640 root root
}

The 90-day audit-record recommendation for cron monitoring is documented in guidance on accessing and analyzing crontab logs. The local rotate count and schedule should be selected to preserve that investigation window, taking compression and central retention into account. A host with a shorter local policy can still meet the operational requirement if a central store receives the records before rotation.

Test the rule rather than trusting its syntax:

sudo logrotate -d /etc/logrotate.d/cronjobs
sudo logrotate -f /etc/logrotate.d/cronjobs
sudo find /var/log/cronjobs -type f -ls

If rsyslog writes the active file, restarting it can interrupt collection if performed carelessly. Validate configuration first, then use the service's documented reload behavior and inspect the queue afterward. Journald has its own retention controls, so logrotate won't manage journal storage.

A four-step diagram illustrating the process of managing and rotating cron job logs on a server.

Central stores preserve context

Local files are useful for immediate shell access, but they disappear with ephemeral containers and can be lost during host replacement. Containerized jobs should write to stdout and stderr, allowing the runtime or collector to ship records centrally with fields such as job name, container or pod identifier, host, and status. File-only output inside a disposable container is an avoidable evidence gap.

Common shipping paths include:

  • Rsyslog forwarding: Use imjournal to read journald and omfwd to send records to a remote syslog endpoint, preserving facility and severity where possible.
  • Journald forwarding: Enable the appropriate forwarding behavior when rsyslog is the collection layer, then verify that messages aren't duplicated or filtered.
  • Filebeat: Target /var/log/cron* and dedicated cron output files, using JSON decoding when the wrapper emits one record per line.
  • Fluent Bit: Tail host-mounted cron files or container stdout, adding Kubernetes metadata before forwarding.

The logrotate complete guide covers the broader mechanics of rotation. Centralization should happen before compression or deletion, and the central record should retain the original timestamp, hostname, account, job name, exit status, and message body. Otherwise a searchable archive can still lack the context needed to prove which scheduled execution failed.

Monitoring and Alerting on Cron Job Failures

A log line is passive evidence. It doesn't wake an operator, distinguish a successful backup from a merely launched process, or detect a schedule that never ran. Production monitoring needs an explicit success signal, an exit status, a runtime boundary, and an absence condition.

The wrapper can emit a machine-readable completion record:

started=$(date +%s)
started_at=$(date --iso-8601=seconds)

set +e
/usr/local/bin/backup.sh >>"$log_file" 2>&1
status=$?
set -e

finished=$(date +%s)
duration=$((finished - started))
finished_at=$(date --iso-8601=seconds))

jq -nc \
  --arg job "database-backup" \
  --arg timestamp "$finished_at" \
  --arg command "/usr/local/bin/backup.sh" \
  --argjson exit_code "$status" \
  --argjson runtime_seconds "$duration" \
  '{job:$job,timestamp:$timestamp,command:$command,exit_code:$exit_code,runtime_seconds:$runtime_seconds}'

The status line should be written only after the command reaches its completion point. A separate started record helps identify hangs, while the final record allows alerting systems to evaluate success without parsing free-form shell output.

Three workable alerting patterns

Heartbeat monitoring uses a unique URL that the job calls only after success. If the call never arrives within the expected window, the monitor raises a missed-run alert. This is simple and effective for scripts that already have a clear final step, but the endpoint call must occur after validation, not immediately after process launch.

Prometheus Pushgateway and Alertmanager can receive gauges for exit code, runtime, and last-success timestamp. This gives teams queryable history and routing rules, but it introduces another component and requires disciplined cleanup for short-lived job labels.

Nagios or Icinga can run checks against expected execution windows and inspect a status file or timestamp. This fits environments that already use plugin-based checks, although each job may require bespoke state handling.

A comparison graphic between passive logging, using simple logs, and active monitoring using advanced alert systems.

Operational distinction: A successful scheduler launch, a successful process exit, and a valid business result are three separate checks.

Absence detection deserves equal attention. A daily schedule that hasn't produced a success record within its expected window should alert even when no failure line exists. A backup wrapper can exit successfully after creating an unusable file, so a post-run validation, such as checking expected contents or transfer completion, should precede the heartbeat.

For teams that want a unified task monitor rather than separate shell checkers, Fivenines can track cron jobs through a unique ping URL that scripts call after successful execution, then alert when an expected ping is late or missing. The alert setup guide covers broader notification workflows, while the operational design remains the same: alert on a verified state transition, not on the existence of a log file.

Pre-Flight Checklist and Common Questions

A cron deployment is ready only when its execution path and evidence path have both been tested. The following checks catch most silent failures before the schedule matters:

  • Daemon: Run systemctl is-active cron or systemctl is-active crond, then confirm the correct service name for the distribution.
  • Backend: Verify /var/log/syslog, /var/log/cron, or journalctl receives a test event. Check rsyslog status and journal persistence where applicable.
  • Environment: Use absolute binary paths, define PATH, set the expected shell and working directory, and validate the job under the same account.
  • Output: Redirect both streams with >> ... 2>&1, add timestamps, and confirm ownership and permissions on the destination.
  • Overlap: Use flock or an equivalent lock so a slow run doesn't overlap the next invocation.
  • Rotation: Run logrotate -d against the custom rule and inspect the resulting file permissions.
  • Alert: Test a deliberate non-zero exit and a missing-success condition before relying on the monitor.

A five-point checklist for verifying cron job deployment readiness, featuring icons for daemon, logging, environment, output, and alerts.

Common questions

Why did cron run the command but create no job output? The crontab may discard output, redirect it to /dev/null, or send it to an unavailable mail system. Check the scheduler line, then add explicit stdout and stderr redirection.

How can an operator tell whether cron is installed? Use command -v cron or command -v crond, then inspect the matching systemd unit with systemctl status.

What's the difference between /var/log/cron and /var/log/syslog? They're distribution and configuration choices. RHEL-family hosts commonly separate cron messages into /var/log/cron, while Debian and Ubuntu commonly place them in /var/log/syslog.

How can a schedule be tested without waiting for its normal time? Temporarily install a frequent test entry that writes to a dedicated temporary log, verify the result, and restore the intended schedule immediately afterward.

Why does MAILTO sometimes produce nothing? Cron may invoke local delivery, but the host might lack a functioning mail transfer agent or the mailbox may not be monitored. File or centralized logging is more dependable for forensic output.


Fivenines provides cron job tracking based on post-success ping signals, with alerts and workflow automation for missed or late executions alongside infrastructure monitoring. Teams responsible for silent backups or unattended maintenance can visit Fivenines to evaluate a centralized way to turn cron execution evidence into actionable task state.

Read more