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
Answer first
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.
Workload and what breaks
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.
Where shortcuts fail
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.
How Inquir helps
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.
Compare
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.
| Dimension | crontab | systemd timers | Kubernetes CronJob | Inquir scheduled pipelines |
|---|---|---|---|---|
| Setup & ops burden | Edit user crontab on a host; no expression validation at save time | Unit + timer files; more reliable firing, still host-level packaging | YAML manifests and a running cluster—even for one nightly job | Cron on a pipeline trigger; expression validated when you save |
| Run history | Redirect stdout to a log file or root mail—no structured audit trail | journalctl per unit; grep across services when jobs multiply | Pod logs plus Job status; retention depends on cluster logging stack | Invocation records beside HTTP executions—queryable without SSH |
| Retries on failure | None—next fire is the only implicit retry | OnFailure= can restart the unit; no per-run retry policy in product UI | Job backoffLimit; tuned in manifest, not beside your API history | Per-step retry count and delay on the pipeline |
| Secrets | .env on the server or inline in shell—shared across jobs on the host | EnvironmentFile units; rotation is still manual per host | Kubernetes Secrets—cluster-native but separate from app gateway auth | Workspace secrets shared with HTTP routes—no parallel server .env |
| Isolation between jobs | One UNIX user and interpreter env—dependency conflicts are common | Can run as different users; still one machine, shared kernel | Pod per run—strong isolation when cluster is already justified | Container per invoke—same isolation model as synchronous HTTP handlers |
| Version control with app code | Crontab lines often live outside deploy pipelines | Unit files can live in git; drift between hosts still happens | Manifests in git/GitOps—excellent when K8s is already the control plane | Function bundles and pipeline config versioned with platform deploy |
| Multi-step workflows | Shell scripts chaining commands—opaque failure points | Separate units or bash in ExecStart—manual orchestration | Single container per CronJob; DAGs need Argo/Workflows or similar | Pipeline graphs with edges—extract, transform, load as function nodes |
| Overlap / long runs | Manual flock or hope—overlapping runs corrupt shared state | Concurrency settings per unit—still DIY in handler logic | concurrencyPolicy Forbid/Replace on the CronJob spec | Skip-if-running guards and idempotency keys in handler or pipeline design |
| Best fit | One VPS, one engineer, scripts that already work and rarely fail | Linux ops teams standardized on systemd who accept journal diving | Products already on Kubernetes with GitOps and cluster logging | Cron plus REST on one platform—history and retries without cluster YAML |
What you get
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.
What to do next
Migration checklist
Move one recurring task safely from shell to scheduled pipeline execution.
Extract handler from shell script
Move script logic into a versioned function.
Test manual invoke + attach schedule
Validate outputs first, then add a schedule trigger.
Add overlap guard + run alert
Prevent concurrent corruption and surface failures immediately.
Code example
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.
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 }; }
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 }; }
When it fits
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
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.