# Cron Jobs

> Run a finite command on a UTC schedule.

Source: https://nexthomelabs.com/docs/services/cron-jobs
Markdown: https://nexthomelabs.com/docs-md/services/cron-jobs
Slug: services/cron-jobs
Section: Create
Last updated: 2026-08-30
Reading time: 5 min read

---

# Cron Jobs

A **Cron Job** starts a command on a schedule, performs a **finite task, and exits**. That finality is the whole point of the service type: the platform measures success by exit code, and the dashboard records per-run history rather than watching for a long-lived process.

Use Cron Jobs for reports, cleanup, data imports, periodic credential rotation, cache warming, newsletter generation, and other synchronization tasks — anything that is naturally described as "run this command at midnight, and when it is done, it is done." For a process that must stay online continuously, choose a [Background Worker](/docs/services/background-workers). For an on-demand container task that already carries a formal timeout and retry limit, choose a [Workflow](/docs/services/workflows).

## When to use — and when not to

**Use a Cron Job when:**

- The task is bounded by time rather than by a queue: nightly email exports, hourly metric rollups, daily provider syncs, weekly pruning.
- The natural success signal is an exit code — `0` means done, non-zero means the run failed and should be retried or investigated.
- The schedule is reasonably described as a cron pattern, not as an event.

**Do not use a Cron Job when:**

- The process must **stay alive** continuously, consuming from a queue or stream — use a Background Worker.
- The deliverable is an HTTP service — use [Web Service](/docs/services/web-services) or [Frontend App](/docs/services/frontend-app).
- The task has nuanced input-by-input retry and timeout expectations better described as a bounded container invocation — use a Workflow.

## Schedule and command

Choose **Cron Job** in **New Project**, configure a supported source or image, set the **command**, and enter a **standard five-field cron expression**. NexHost schedules cron jobs in **UTC**, so convert business-time requirements before saving the schedule.

The canonical fields — with no extension:

```text
*  *  *  *  *
│  │  │  │  └─ day of week (0–7, both 0 and 7 mean Sunday)
│  │  │  └──── month (1–12)
│  │  └─────── day of month (1–31)
│  └────────── hour (0–23, UTC)
└───────────── minute (0–59)
```

Practical examples at the dashboard boundary (UTC):

| You want | Cron expression | Note |
| --- | --- | --- |
| Every hour at the top of the hour | `0 * * * *` | Good probe for "is the pipeline still progressing?" |
| Daily at 02:00 UTC | `0 2 * * *` | Convert your local-time requirement to UTC — e.g. "02:00 Europe/Berlin in UTC" means a different hour in winter versus summer due to DST. |
| Every Monday at 06:15 UTC | `15 6 * * 1` | Great for weekly rollups; verify the day mapping against the help tooltip for the dashboard. |
| Every 10 minutes | `*/10 * * * *` | High-frequency polling belongs in a Worker if the cadence is about "keep reading," not "run this discrete report." |

> [!WARNING]
> The platform makes the decision on UTC. Do not author the expression for your wall-clock time zone and assume it will track local daylight changes — convert once, label it in the schedule’s description if your process description holds that context, and verify the first invocation’s logged UTC timestamp matches the converted time.

The **command** itself follows the same source conventions as other runtime services: for a repository/archive source the dashboard expects a command string that makes sense at the project’s root (for example `python scripts/cleanup.py` or `node scripts/import.js`). For an existing-image source, the image’s entrypoint may be the command, with these settings as its arguments. Inspect the deployment detail after the first scheduled run — some images expose the effective `CMD` as it was assembled, which is faster than re-deriving it from `Dockerfile` history.

## Good job behavior

A well-behaved cron job is one your future on-call can diagnose from one run’s output and can safely retry after a partial failure.

- **Make each run safe to retry.** Structure the operation to be **idempotent** — re-running the same command with the same input should not double-count, double-bill, or synthesize duplicate side effects. Prefer "process every unprocessed row" over "process row N" where possible, and guard external mutations with idempotency keys.
- **Exit with a non-zero code on failure.** The platform reports exit status per run in history. A job that exits `0` even when the write failed reads as successful in history and will not draw attention when it silently stops delivering value. Let expected errors bubble to a non-zero exit.
- **Write actionable application output.** Print what the run consumed, what it produced (count of processed items, skipped items, and failures), and where a retried run would start. Future diagnostics — and the diff between two runs — benefit more from that ledger than from verbose debug noise.
- **Keep per-run dependencies lean.** A nightly import that hits an external provider once at startup and then reuses the connection is cheaper than one that establishes N connections inside N iterations. In low-resource contexts this is also friendlier to shared-host limits.

Use a [Background Worker](/docs/services/background-workers) for queue consumers or any process that **should not exit after a single run** — a worker that has a schedule to retry after a transient error long before the next cron tick belongs on a continuous process.

> [!TIP]
> Simulate the schedule locally with a single manual run: `cron_expression="0 2 * * *" python scripts/cleanup.py --dry-run` or your language’s equivalent scheduler harness. Observing idempotent output on a second run in the same checkout outs the class of bugs that midnight-only invocation would hide.

## Observing runs

The **deployment detail / service history** records each cron invocation, its inputs (per the source that was recorded for the deployment), the command as configured, the exit code, and the captured application output. Inspect a **failed run before retrying** so a configuration or input problem is not repeated automatically — especially for destructive jobs such as cleanup or migration, where re-running the same bad inputs compounds harm.

## Related documentation

- [Background Workers](/docs/services/background-workers) — continuous consumers when "staying alive" is the correct model.
- [Workflows](/docs/services/workflows) — on-demand bounded container tasks with explicit timeout and retry limit.
- [Logs](/docs/operations/logs) — reading stage output and application lines for a scheduled rather than an HTTP-triggered service.
- [Deployments](/docs/deployments) — per-run provenance and comparison after the next run.

