An e-commerce shop discovers on Monday that no order was exported to the ERP for twelve days. The cron script still "runs": crontab entry correct, log file dated this morning. In reality the job fails on SFTP connection after a key change — but the developer did not redirect stderr, nobody watches exit codes, and shared hosting does not notify.
Cron tasks are invisible by design. They run at night with no user watching. That is exactly why they need more observability than a web page — not less.
Anatomy of production cron
| Element | Bad practice | Good practice |
|---|---|---|
| Output | /dev/null | Structured log + rotation |
| Failure | Ignored | Alert if exit ≠ 0 |
| Duration | Unmeasured | Timeout + overrun alert |
| Concurrency | Double run | flock / Redis lock |
| Idempotency | No | Replay without double effect |
A cron that "runs" without business success proof did not run — it only consumed CPU.
Shared vs VPS: where your night runs
Shared:
- Low schedule precision; overlap with neighbor jobs.
- Cron
mail()often disabled or filtered. - No advanced systemd timers.
VPS / cloud:
- systemd timers with
Persistent=true(catch-up). - Centralized journald logs.
- Dedicated workers for heavy jobs.
For billing or stock sync, shared hosting is a gamble. For weekly cache purge, acceptable.
Minimal observability pattern
- Shell wrapper:
#!/bin/bash
set -euo pipefail
LOG=/var/log/jobs/export-erp.log
{
echo "=== $(date -Is) start ==="
/usr/bin/php /app/bin/export-erp.php
echo "=== $(date -Is) ok ==="
} >> "$LOG" 2>&1 || curl -fsS -m 10 --retry 3 https://hc.example/ping/xxx-fail
- Healthchecks.io / Cronitor — ping START and SUCCESS; alert if missing.
- Duration metric — Grafana or parsed log; alert if > 2× median.
- Runbook — link in alert to manual procedure.
See Crontab and locking and Celery: keep a task queue from becoming a black box.
Idempotency and locks
Classic scenario: product import takes 25 min, cron every 15 min → two imports corrupt data.
Solutions:
- flock -n at script start — skip if already running.
- Redis SET NX with TTL > max job duration.
- PostgreSQL advisory lock for DB-centric jobs.
Every critical job must be replayable: rerun without doubling writes (UPSERT, batch_id marker).
Cron → queue migration
| Signal | Action |
|---|---|
| Job > 5 min | Worker + queue |
| Retry with backoff | Celery / Messenger |
| Job dependencies | Orchestrator (Temporal, light Airflow) |
| Team visibility | Flower / Horizon dashboard |
Cron stays useful to trigger (0 2 * php bin/console messenger:consume) — not to run everything inline.
The peak: cron failure is always discovered by business, never by tech
The host sells "cron included." Your job: prove success or failure independent of the hosting panel.
Decide and move forward without blind spots
- Inventory all crons (crontab + panel + CI).
- Classify criticality and add external healthcheck to critical ones.
- Lock jobs > 1 min or short interval.
- Test deliberate failure — alert within 5 min?
- Document owner and runbook per job.
Compare PaaS with observable cron (Alwaysdata) via our directory.
Frequently asked questions
Why is shared hosting cron unreliable?
Imprecise windows, shared load, no failure notification — risky for critical sync.
How do you know a cron failed?
Logged exit code + external ping or alert; silence is not success.
Do you need a lock to prevent duplicates?
Yes if job duration ≥ interval — flock, Redis, or advisory lock.
System cron or queue?
Cron for short and rare; queue + workers for long, retry, visibility.
Reliable cron is not seen at night — it proves itself in the morning, or alerts before business notices.