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

OptionTrigger costRuns your code?The part that bites
crontab on a small VPSwhatever the box costsyes, on your boxpatching, disk, and one machine that can die quietly
GitHub Actions schedulefree minutes on public reposyes, in a runnerschedule drift under load, and minutes billed on private repos
Cloudflare Workers Cron Triggersfree at low volumeyes, in a WorkerWorker CPU limits reshape how you write the cleanup
EventBridge Schedulerper-invocation, free tier firstno — it invokes a targetyou still need the Lambda, its role, and its log group
Infrai POST /v1/cron/createfree, rate-limitedno — it POSTs to your URLyour 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.

References

Browse more cron developer guides