Use case

Serverless cron jobs on the same platform as your APIs

Run serverless cron jobs as scheduled pipelines with run history next to HTTP invocations, retries you can tune, and shared secrets and logs—so nightly work uses the same functions as your REST API endpoints.

Last updated: 2026-06-28

Direct answer

Serverless cron jobs on the same platform as your APIs. The same function ID can power an HTTP route and a scheduled pipeline step—no duplicate code trees.

When it fits

  • Nightly ETL
  • Certificate or token rotation
  • Periodic polling integrations

Tradeoffs

  • Timers improve firing reliability, but they do not give you dependency isolation or the shared secrets and logs your HTTP handlers already use.
  • You still need one place for retries, run history, and alerts—otherwise scheduled jobs stay invisible next to production APIs.

Why hosted cron and crontab fail

  • VPS crontab: output routed to root mail; no built-in retries
  • systemd timers: reliable firing, but scattered logs and manual secrets
  • Kubernetes CronJob: solid primitives with cluster overhead for small teams

Cron jobs fail silently when their only output lands in root mail or ad-hoc log files that nobody reads.

If you keep schedules only on a VPS, you own packaging drift, secret sprawl, and “who restarted cron?” debugging—every outage becomes SSH archaeology.

Crontab entries drift outside versioned deploy workflows, so no one knows which binary ran last night.

Overlapping runs corrupt shared state without skip-if-running guards—scheduled pipelines need the same rigor as webhook processors.

Why timers alone are not enough

Timers improve firing reliability, but they do not give you dependency isolation or the shared secrets and logs your HTTP handlers already use.

You still need one place for retries, run history, and alerts—otherwise scheduled jobs stay invisible next to production APIs.

One platform for APIs and schedules

The same function ID can power an HTTP route and a scheduled pipeline step—no duplicate code trees.

Cron strings are validated when you save the pipeline; the worker tracks nextRunAt per pipeline so edits reschedule cleanly and run history stays queryable.

Crontab vs systemd timers vs Kubernetes CronJob vs Inquir

Teams pick a scheduler based on ops maturity and where the rest of the stack lives. This table contrasts four common approaches for recurring backend work—not edge cron or sub-second tickers.

Crontab vs systemd timers vs Kubernetes CronJob vs Inquir
Dimensioncrontabsystemd timersKubernetes CronJobInquir scheduled pipelines
Setup & ops burdenEdit user crontab on a host; no expression validation at save timeUnit + timer files; more reliable firing, still host-level packagingYAML manifests and a running cluster—even for one nightly jobCron on a pipeline trigger; expression validated when you save
Run historyRedirect stdout to a log file or root mail—no structured audit trailjournalctl per unit; grep across services when jobs multiplyPod logs plus Job status; retention depends on cluster logging stackInvocation records beside HTTP executions—queryable without SSH
Retries on failureNone—next fire is the only implicit retryOnFailure= can restart the unit; no per-run retry policy in product UIJob backoffLimit; tuned in manifest, not beside your API historyPer-step retry count and delay on the pipeline
Secrets.env on the server or inline in shell—shared across jobs on the hostEnvironmentFile units; rotation is still manual per hostKubernetes Secrets—cluster-native but separate from app gateway authWorkspace secrets shared with HTTP routes—no parallel server .env
Isolation between jobsOne UNIX user and interpreter env—dependency conflicts are commonCan run as different users; still one machine, shared kernelPod per run—strong isolation when cluster is already justifiedContainer per invoke—same isolation model as synchronous HTTP handlers
Version control with app codeCrontab lines often live outside deploy pipelinesUnit files can live in git; drift between hosts still happensManifests in git/GitOps—excellent when K8s is already the control planeFunction bundles and pipeline config versioned with platform deploy
Multi-step workflowsShell scripts chaining commands—opaque failure pointsSeparate units or bash in ExecStart—manual orchestrationSingle container per CronJob; DAGs need Argo/Workflows or similarPipeline graphs with edges—extract, transform, load as function nodes
Overlap / long runsManual flock or hope—overlapping runs corrupt shared stateConcurrency settings per unit—still DIY in handler logicconcurrencyPolicy Forbid/Replace on the CronJob specSkip-if-running guards and idempotency keys in handler or pipeline design
Best fitOne VPS, one engineer, scripts that already work and rarely failLinux ops teams standardized on systemd who accept journal divingProducts already on Kubernetes with GitOps and cluster loggingCron plus REST on one platform—history and retries without cluster YAML

Scheduling, retries, and observability

Cron validation

Schedule expressions are validated at save time so malformed entries fail early.

Worker cadence and overlap handling

Design minute-scale workloads and add skip-if-running guards for long jobs.

Execution history

Answer “did the job run?” without grep—run history sits with HTTP executions.

Run alerts

Hook monitoring to failure rates or duration SLOs for scheduled jobs.

Migration checklist

Move one recurring task safely from shell to scheduled pipeline execution.

1

Extract handler from shell script

Move script logic into a versioned function.

2

Test manual invoke + attach schedule

Validate outputs first, then add a schedule trigger.

3

Add overlap guard + run alert

Prevent concurrent corruption and surface failures immediately.

Common cron job patterns

Scheduled handlers receive pipeline metadata on event. Two common patterns: watermark-based incremental sync (safe to retry) and report generation with fan-out.

jobs/incremental-sync.mjs (watermark pattern)
export async function handler(event) {
  // Read cursor from env so re-runs do not re-process old records
  const since = process.env.SYNC_CURSOR ?? new Date(Date.now() - 86_400_000).toISOString();
  const records = await source.fetchUpdatedSince(since);
  await destination.upsertBatch(records); // idempotent by record ID
  const newCursor = records.at(-1)?.updatedAt ?? since;
  // Update cursor in your config/store for next run
  return { synced: records.length, cursor: newCursor };
}
jobs/nightly-report.mjs (report + fan-out)
export async function handler(event) {
  const rows = await buildReport();
  await storage.upload(rows, { key: `reports/${new Date().toISOString().slice(0, 10)}.csv` });
  // Fan out — each recipient gets a separate pipeline step
  await Promise.all(
    recipients.map((r) => global.durable.startNew('send-report', undefined, { recipientId: r.id, rowCount: rows.length })),
  );
  return { rows: rows.length, notified: recipients.length };
}

Choose this when…

When this works

  • Nightly ETL
  • Certificate or token rotation
  • Periodic polling integrations

When to skip it

  • Sub-second periodic tasks—validate platform timers first

FAQ

What if a cron run takes longer than the interval?

Use idempotency keys, distributed locks, or skip-if-running guards inside the handler so overlapping firings do not corrupt shared state.

How is this better than crontab on a single machine?

You get versioned lambda bundles, container isolation per invoke, persisted invocation records, and the same secrets model as HTTP functions—plus multi-step graphs when one schedule should fan out.

Which timezone applies to cron expressions?

Document the timezone your team expects (often UTC for backends); align schedules with daylight-saving rules if you target wall-clock business hours.