Scheduling email digests with cron
Daily or weekly digest sends, timezone-aware, with delivery failures treated as separate from generation.
The problem
Digest emails have two independent failure points that get treated as one: generating the content (querying what happened, rendering the email) and delivering it (SMTP or an email API call). A script that logs "digest sent" the moment it hands off to the mail server, without checking the send actually succeeded, will report success on days the email never left the building.
For a digest meant to land at a consistent local time for a distributed audience, cron's system-clock timezone becomes a real design decision — a server on UTC sending "0 8 * * *" delivers at 8am UTC, not 8am for any particular user, and that drifts further wrong across daylight saving changes if you assume otherwise.
Recommended schedule
0 8 * * *
Daily at 08:00 server time. If your audience is in one timezone, set TZ= on the crontab line (or in the script) explicitly rather than relying on the system default — see /tools/cron-expression-parser to confirm the resulting UTC offset.
Check any expression at the cron parser, or browse more at the cron guide.
Example crontab entry
# norc: email-digest Daily activity digest
0 8 * * * TZ=America/New_York /usr/bin/python3 /opt/digest/send.py >> /var/log/digest.log 2>&1 result = send_digest_emails()
if result.failed_count > 0:
raise SystemExit(f"{result.failed_count} digest sends failed") # nonzero exit, not just a log line Failure modes to watch for
- Send failures logged but not surfaced as a nonzero exit code, so the job "succeeds" from cron's perspective on a day half the digests bounced.
- Timezone assumed rather than set explicitly, delivering at a fixed UTC time that reads as a different local hour after a DST transition.
- No idempotency guard: a cron job that's manually re-run to recover from a partial failure re-sends digests to recipients who already got theirs.
- Rate limits from the email provider hit partway through a large recipient batch, silently truncating the send with no per-recipient retry.
How norc helps
norc's run history separates "the digest job ran" from "it exited cleanly," so a script that starts silently swallowing send failures shows up as a failed run instead of a slow trickle of support tickets.