Cheapest cron for nightly data cleanup: Actions, Workers or EventBridge?
GitHub Actions cron, Cloudflare Workers Cron Triggers, EventBridge Scheduler and Infrai compared on a nightly cleanup job — and what each one really bills you for.
For a nightly cleanup the scheduler is rarely the line item worth optimising. GitHub Actions cron, Cloudflare Workers Cron Triggers, EventBridge Scheduler and Infrai’s cron routes will all fire a daily task for free or for a rounding error. What you pay for is the compute that wakes up — and the hours you spend keeping the thing alive.
So the useful comparison isn’t a price list. It’s a list of what each option makes you responsible for, and Infrai’s position in that list is narrow on purpose: it schedules an HTTP POST and records what happened, nothing more.
What the trigger costs versus what the job costs
| Option | Trigger cost | Runs your code? | The part that bites |
|---|---|---|---|
crontab on a small VPS | whatever the box costs | yes, on your box | patching, disk, and one machine that can die quietly |
GitHub Actions schedule | free minutes on public repos | yes, in a runner | schedule drift under load, and minutes billed on private repos |
| Cloudflare Workers Cron Triggers | free at low volume | yes, in a Worker | Worker CPU limits reshape how you write the cleanup |
| EventBridge Scheduler | per-invocation, free tier first | no — it invokes a target | you still need the Lambda, its role, and its log group |
Infrai POST /v1/cron/create | free, rate-limited | no — it POSTs to your URL | your endpoint has to be publicly reachable |
Two rows there don’t run anything. That’s the split worth internalising: EventBridge Scheduler and Infrai cron are triggers, and a trigger is cheap because somebody else is holding the compute. Actions and Workers bundle the runtime, which is convenient right up to the point where your cleanup takes eleven minutes and the runtime says no.
Cleanup jobs tend to grow that way.
The nightly job, as one request
Delete uploads older than 30 days at 03:15 Berlin time. The job posts to an endpoint you already own; payload is handed back to you verbatim inside the delivery envelope.
export INFRAI_API_KEY="your_infrai_api_key"
curl -sS -X POST "https://api.infrai.cc/v1/cron/create" \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"name": "nightly-uploads-cleanup",
"cron_expr": "15 3 * * *",
"timezone": "Europe/Berlin",
"task_type": "http_url",
"task": "https://ops.example.com/internal/cleanup/uploads",
"payload": { "older_than_days": 30, "dry_run": false },
"overlap_policy": "skip",
"retry": 2,
"timeout_seconds": 600
}'
The field is task, not task_url — the response echoes it back as task_url, which is a nice way to lose twenty minutes if you write the response shape into your request.
{
"ok": true,
"data": {
"job_id": "cron_4NqAOHauNa6aZqjdMTxjLHMI",
"name": "nightly-uploads-cleanup",
"cron_expr": "15 3 * * *",
"task_type": "http_url",
"task_url": "https://ops.example.com/internal/cleanup/uploads",
"timezone": "Europe/Berlin",
"retry": 2,
"timeout_seconds": 600,
"overlap_policy": "skip",
"enabled": true,
"status": "active",
"next_run_at": null
}
}
next_run_at comes back null for recurring jobs — it’s populated for one-shots created with run_at. Check status and enabled instead.
Confirming it exists
curl -sS "https://api.infrai.cc/v1/cron/list" \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
curl -sS "https://api.infrai.cc/v1/cron/runs/list/cron_4NqAOHauNa6aZqjdMTxjLHMI" \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
Run history is where the money question gets answered honestly, because it tells you whether the cleanup is finishing or quietly timing out every night at 600 seconds.
{
"ok": true,
"data": {
"items": [
{
"run_id": "cronrun_f3Jr8IFSZmTKQTTUi6hzW23N",
"job_id": "cron_4NqAOHauNa6aZqjdMTxjLHMI",
"fired_at": "2026-07-26T01:22:53.777462Z",
"status": "failed",
"http_status": 503,
"duration_ms": 0,
"error_code": "CRON_TASK_URL_UNREACHABLE"
}
],
"next_cursor": null
}
}
Reading today’s rate rather than trusting this page
Every cron route on Infrai is free and rate-limited, and — checked on 2026-07-26 — it doesn’t draw down the $2 new-account credit either. The metered neighbour is queue.publish at $0.00002 per message, which matters the moment your cleanup fans out into per-tenant jobs. Rates here move down over time and discount campaigns run, so treat both numbers as a ceiling and go read the live ones:
import process from "node:process";
const key = process.env.INFRAI_API_KEY;
if (!key) throw new Error("set INFRAI_API_KEY");
const res = await fetch("https://api.infrai.cc/v1/discovery", {
headers: { Authorization: `Bearer ${key}` },
});
if (!res.ok) throw new Error(`discovery failed: HTTP ${res.status}`);
const { capabilities } = await res.json();
for (const cap of capabilities) {
if (!/^(cron|queue)\./.test(cap.id)) continue;
const b = cap.billing ?? {};
const rate = b.is_billable ? `$${b.price_usd} / ${b.unit}` : "free";
console.log(`${cap.id.padEnd(22)} ${rate}`);
}
And for what you’ve actually spent, rather than what a table claims:
curl -sS "https://api.infrai.cc/v1/account/usage" \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
Where this is the wrong pick
Infrai cron can’t run your cleanup. It makes an HTTP call and stores the result; timeout_seconds caps at 900, so a job that needs an hour has to become a job that enqueues work. If your data lives in AWS and the cleanup is already a Lambda, EventBridge Scheduler is fewer moving parts than adding an HTTPS route just to receive a POST. If you want retries expressed as a workflow with durable steps, that’s Temporal or Inngest territory and you’d be better off there. And a single crontab line on a box you already run is genuinely hard to beat — right up to the second box.
The argument for putting it on Infrai isn’t the zero rate. It’s that step two of the cleanup — enqueue the deletions, drop the report in storage, email the summary, capture the failure — is the same key and the same bill, so per-tenant cost attribution stays a query instead of a reconciliation across four vendors.
Pause it when you’re mid-migration and don’t want the sweep running:
curl -sS -X POST "https://api.infrai.cc/v1/cron/pause/cron_4NqAOHauNa6aZqjdMTxjLHMI" \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
-H "Content-Type: application/json" \
-d '{}'
POST /v1/cron/resume/{id} puts it back. There’s no backfill for the runs you skipped — a paused schedule is time lost, not time deferred, and the same is true of any minute the scheduler misses.