Skip to content

Guides

Cron and jobs

Markdown

Run something of an app on a schedule, by calling a path or by running a command on a machine of its own, and run one-off commands as jobs with their exit code and output kept.

A schedule runs on a cron expression. It either wakes the app and calls a path of it, signed so the app can verify the call, or it runs a command on a machine of its own, built from the app’s image and environment. A job is that same machine without a schedule: one command, run once.

For frequent, light work the app does itself, down to every minute.

ry cron add web "*/15 * * * *" --path /api/tick

The expression has five fields (minute, hour, day of month, month, day of week) or is @hourly, @daily and the like, read in the timezone you name (default UTC, for example Europe/Berlin). method is GET (default) or POST. Each attempt has timeoutSec (default 60, at most 240).

A path is called through the app’s address with these headers:

Header Value
Rytiv-Cron-Signature v1=<hex>: HMAC-SHA256 of <timestamp>.<path> under the app’s secret RYTIV_CRON_SECRET
Rytiv-Cron-Timestamp Unix seconds when the call was signed
Rytiv-Cron-Run The run’s id, the same across retries
Rytiv-Cron-Schedule The schedule’s id
Rytiv-Cron-Scheduled-For The slot the run is for

<path> is the path with its query, exactly as sent. The secret RYTIV_CRON_SECRET is created as a managed secret with the app’s first path schedule; the app sees it from its next deploy, and a run before that fails rather than being sent unsigned.

import { createHmac, timingSafeEqual } from "node:crypto";
export function cronCallIsGenuine(req: Request, path: string): boolean {
const ts = req.headers.get("rytiv-cron-timestamp") ?? "";
const sig = req.headers.get("rytiv-cron-signature") ?? "";
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const expected = `v1=${createHmac("sha256", process.env.RYTIV_CRON_SECRET!).update(`${ts}.${path}`).digest("hex")}`;
return expected.length === sig.length && timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
}

A call is retried up to three times (after 5 s, then 10 s) on a network error or a 5xx; a 4xx or a redirect is not retried. A slot that comes while the previous run of the same schedule still runs is skipped, not queued; a slot more than 10 minutes late counts as missed.

For heavy, rare work: at most hourly, and every run gets a machine of its own, sized as you say.

ry cron add web "0 3 * * *" --timezone Europe/Berlin --command "npm run cleanup" --memory 8192
ry cron add web "@daily" -- node scripts/report.js --full

--command runs through /bin/sh -c; the form after -- is the argument list without a shell. The machine runs the app’s image with its secrets and environment, and the app’s size unless --memory or --vcpus say otherwise. A run has timeoutSec (default 900, at most 3600). The minute field of the expression must name one minute.

ry cron list web
ry cron runs web sch-1a2b3c4d
ry cron run web sch-1a2b3c4d
ry cron pause web sch-1a2b3c4d
ry cron resume web sch-1a2b3c4d
ry cron rm web sch-1a2b3c4d

An app has at most 20 schedules; the last 100 runs of each are kept. ry cron run starts a run now, outside the expression (also on a paused schedule); resuming continues from the next slot, nothing missed is caught up. A run is running, succeeded, failed, skipped or missed. Removing a schedule takes its history with it.

ry job run web -- npm run migrate
ry job run web --memory 4096 --timeout 1800 -- node scripts/report.js

A job runs the command on a machine of its own with the app’s image, secrets and environment, and answers 202 with the job. ry job run streams the output and exits with the command’s own exit code (1 for failed or timeout); Ctrl-C leaves the job running. Poll GET /v1/jobs/:id until state is done, failed or timeout:

State Meaning
queued The machine is being placed.
running The command runs; startedAt is set.
done The command ran to its end; exitCode holds its verdict (a failing command is done with a non-zero code).
failed It did not run to the end: placement failed, the machine died, or the job was cancelled. exitCode is null, the reason is the last line of output.
timeout timeoutSec ended it.

“Green” means state === "done" && exitCode === 0. output keeps the first 1 MiB. In the API, env adds environment for this job, links names apps the job’s machine may reach, build runs a specific build of the app; the CLI offers -e, -t, -m and -c.

ry job list web
ry job get job-1a2b3c4d
ry job cancel job-1a2b3c4d

Cancelling (DELETE /v1/jobs/:id) ends a running job; its machine is gone after finishedAt, the job record stays.