Scheduling recurring report generation
Weekly or monthly report jobs, and the date-math bugs that make them wrong on specific days.
The problem
Report jobs look simple — run a query, render a file, email or upload it — but they fail in ways that are easy to miss because a wrong report still looks like a report. The most common bug is date-range math: a "last 7 days" report computed as `today - 7 days` to `today` silently double-counts or drops a day around daylight saving transitions if the script uses local time instead of a fixed offset or UTC.
The second common bug is cron's own `*` day-of-month vs day-of-week interaction: `0 6 1 * *` for "first of the month" is unambiguous, but a job intended to run "the last day of the month" has no native cron expression — cron has no `L` (last) operator like some other schedulers — so it needs script-side logic (check if tomorrow is day 1) rather than a clever cron field.
Recommended schedule
0 6 * * 1
Weekly, Monday at 06:00, well before the workday starts and after weekend data has settled. For "first of the month" reports use `0 6 1 * *` instead — see /tools/cron-expression-parser to check any variant before deploying it.
Check any expression at the cron parser, or browse more at the cron guide.
Example crontab entry
# norc: weekly-report Weekly usage report
0 6 * * 1 TZ=UTC /usr/bin/python3 /opt/reports/weekly.py >> /var/log/reports.log 2>&1 # weekly.py computes its range in UTC explicitly, not local time,
# so DST transitions never shift the report boundary by an hour
end = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0)
start = end - timedelta(days=7) Failure modes to watch for
- Local-time date math shifting the report window by an hour across a DST transition, double-counting or dropping data at the boundary.
- No file-size or row-count sanity check before delivery, so an empty or partial report (e.g. a query that silently returned zero rows because a table name changed) still gets emailed as if it were valid.
- Report delivery (SMTP, S3 upload) failing separately from generation — the report renders correctly but nobody receives it, and the only log entry is buried in a mail spool.
- "Last day of month" logic hardcoded to day 28, 30, or 31 instead of computed, quietly skipping or misfiring in different-length months.
How norc helps
norc's per-run history distinguishes "job ran and exited 0" from "job actually completed on schedule," so a report script that's silently stopped running shows up immediately instead of during next quarter's review.