Why cron's own email can miss a failure
Cron can email what a job prints. The crontab manual says:
If MAILTO is defined (and non-empty), mail is sent to the specified address. If MAILTO is defined but empty (MAILTO=""), no mail is sent. Otherwise, mail is sent to the owner of the crontab.
And the cron manual says “The syslog output will be used instead of mail, when sendmail is not installed.” So on a server with no mail set up, a job's output goes to the system log or is thrown away, depending on which cron the server runs. A job that fails without printing anything sends nothing at all.
A run that never starts prints nothing either: the server was off, or someone changed the crontab. A daylight saving change can skip one too:
Note that this means that non-existent times, such as the "missing hours" during the daylight savings time conversion, will never match, causing jobs scheduled during the "missing times" not to be run.
To avoid that, schedule jobs outside the hour when your clocks change, or keep the server in UTC.
Sereno knows when each run is due. When no report arrives in time, it marks the run missed and alerts you.
Before you start
- A check in Sereno for this job, on the same schedule. Debian's crontab manual says “The cron daemon runs with a defined timezone. It currently does not support per-user timezones.” Cron uses the server's own time zone, often UTC. Give the check that zone's full name, like America/New_York:
timedatectlshows it, and so doesreadlink /etc/localtime(the part after zoneinfo/). If neither shows a name anddatesays UTC, use UTC. Don't use the short namedateprints, like EST. EST has no daylight saving time, so the check would expect the job an hour off while daylight saving time is on. Make it with New check on your dashboard, which takes the same five-field cron expression, or let your AI agent set it up. - A grace period of 10 minutes plus the job's longest run time. Grace is how late a run's first report can come before the run counts as missed, and the cron line below sends its report only when the job ends. alerts-run also reports when the job starts, so with it 10 minutes is enough, and the check's max run time covers the rest.
- The report line from the check's page, with your project's key and your check's slug filled in.
Add the report line
Swap the job's line in your crontab for this one:
A crontab line (crontab -e, as the user the job runs as). It runs the job, then sends its exit code (0 is ok, anything else a failure).
0 2 * * * /path/to/your-job; rc=$?; curl -fsS -m 10 --retry 5 --retry-connrefused --retry-delay 10 --retry-max-time 60 -o /dev/null "https://asksereno.com/k/PROJECT_KEY/CHECK_SLUG/$rc?rid=$(date +\%s)-$$"0 2 * * * /path/to/your-job; rc=$?; curl -fsS -m 10 --retry 5 --retry-connrefused --retry-delay 10 --retry-max-time 60 -o /dev/null "https://asksereno.com/k/PROJECT_KEY/CHECK_SLUG/$rc?rid=$(date +\%s)-$$"
Keep your own schedule in front (this one runs at 02:00 every day), and put your job's command where /path/to/your-job is. Copy the rest as it is, with your project's key in place of PROJECT_KEY and your check's slug in place of CHECK_SLUG. The check's page shows the line with both filled in. The backslash before each % matters: Debian's crontab manual says a % in the command, “unless escaped with backslash (\), will be changed into newline characters”.
To use alerts-run instead, download it once onto the machine that runs the job. /usr/local/bin usually needs root: run this as root, or put sudo before curl and chmod.
curl -fsS -m 10 --retry 5 --retry-connrefused --retry-delay 10 --retry-max-time 60 -o /usr/local/bin/alerts-run https://asksereno.com/alerts-run && chmod +x /usr/local/bin/alerts-runcurl -fsS -m 10 --retry 5 --retry-connrefused --retry-delay 10 --retry-max-time 60 -o /usr/local/bin/alerts-run https://asksereno.com/alerts-run && chmod +x /usr/local/bin/alerts-run
Then use this crontab line, which wraps the job with alerts-run, a short sh script that needs curl. It also reports when the job starts, sends the job's last lines of output, and exits with the job's own exit code. A run that takes longer than the check's max run time (1 hour unless you change it) counts as failed.
0 2 * * * ALERTS_URL=https://asksereno.com/k/PROJECT_KEY /usr/local/bin/alerts-run CHECK_SLUG -- /path/to/your-job0 2 * * * ALERTS_URL=https://asksereno.com/k/PROJECT_KEY /usr/local/bin/alerts-run CHECK_SLUG -- /path/to/your-job
You can read the alerts-run script first. Download it before the job's next run, and keep your own schedule in front of its line too. With each run's result it sends the end of the job's output, up to 2 KB, and Sereno hides the passwords and keys it recognizes before saving it.
Send a test report
On the server where the job runs, as the user the job runs as, run this. The check's page in Sereno shows this line with your project's key and your check's slug filled in, under Check it can reach us.
curl -fsS -m 10 https://asksereno.com/k/PROJECT_KEY/CHECK_SLUG/testcurl -fsS -m 10 https://asksereno.com/k/PROJECT_KEY/CHECK_SLUG/test
It prints OK. On the check's page, Test report gets a tick. A test report never changes the check's status.
This test also proves that server can reach Sereno. The first scheduled run gives Real run a tick, and the check shows OK, or Failing if the run failed.
If it doesn't work
The test fails
- The test prints
curl: (22) The requested URL returned error: 404. Sereno doesn't know that address. Open the project's page in Sereno: if the test shows under Reports for checks that don't exist, the slug is wrong. If it doesn't show there, the project key is wrong. - The test prints error 429. That check got more than 60 reports in a minute: wait a minute and try again. An error of 500 or above means Sereno couldn't answer just then, so try again in a few minutes. Every answer a report address gives is in the docs.
- The test times out or can't connect. That server can't reach Sereno: check that its firewall or proxy lets it make outgoing web requests to asksereno.com.
- The server says curl isn't found. Install curl with the server's package manager, then run the test again.
The test works, but scheduled runs don't report
- The cron line's reports never arrive. Cron doesn't load your login's settings, so a proxy your login sets, like
https_proxy, isn't set for cron. Set it at the top of your crontab with a line likehttps_proxy=http://PROXY_HOST:PORT, using your proxy's full address and noexportin front. - The job works when you run it, but fails from cron. Cron runs jobs with its own shell and PATH, not your login's. Debian's crontab manual says “SHELL is set to /usr/bin/sh, and LOGNAME and HOME are set from the /etc/passwd line of the crontab's owner.” Use full paths in the line, or set PATH at the top of your crontab, written out in full. Debian's crontab manual also says lines like
PATH=$HOME/bin:$PATH“will not work as you might expect”. - The check's page shows runs as missed, with no report from them, and nothing says why. Cron mails a line's errors, like alerts-run not found, only where the server can send mail. Otherwise look for cron's lines in the system log, with
journalctl -u cronon Debian and Ubuntu, orjournalctl -u crondon Fedora and Red Hat. A line with CMD in it means cron started the job. Some crons log the job's errors there too, and others throw them away, as above. If you use alerts-run, check that /usr/local/bin/alerts-run is there, and download it again if it isn't. - The check stays OK though the job failed. Sereno reads the job's exit code: 0 is ok, anything else a failure. Make the job exit with a code other than 0 when it fails.
The short version of this setup, with the other platforms', is in Sereno's setup recipes.
The cron manuals quoted as read on October 26, 2026.
More guides
- Ask your AI agent why a cron job failed
- Monitor Vercel cron jobs
- Monitor GitHub Actions scheduled workflows
- Monitor Supabase cron jobs that call Edge Functions
- Healthchecks.io alternative with AI agent setup
- Cronitor alternative with 200 free checks
New here? Sign up with your email. Free: up to 200 checks, 50 alert emails a day, and phone alerts through ntfy.