Cron jobs vs background workers vs workflows: picking the right async primitive
Choosing between cron jobs, background workers, and workflows
Most teams pick their async primitive by familiarity: the developer who knows cron schedules everything, the one who shipped Sidekiq queues everything. But familiarity isn't the right test. Instead of asking "which tool do I know?" ask "what happens when this fails halfway through?"
The three primitives answer different questions. Cron jobs answer when work runs. Background workers answer how much work you can absorb. Workflows answer what happens if step 3 fails after step 2 succeeded. Each has distinct failure semantics, retry behavior, and cost profiles, and each breaks down at a predictable seam.
This is a decision framework, not a tutorial. Use it to reason about failure modes before you choose a primitive, rather than after a duplicate charge or a silently skipped nightly report forces the issue. The examples use Render's service types, but the reasoning applies anywhere.
What each primitive actually guarantees
The value of picking correctly comes down to how each primitive behaves under failure. Read this section as three contracts, then match your workload to the one whose guarantees you need.
Cron jobs: time-triggered, stateless runs
Choose a cron job when the trigger is time and each run is self-contained. Nightly aggregations, weekly cleanup, hourly cache warming: no run needs to know what the previous run did.
The failure semantics are blunt. A run either completes or it doesn't, and there's no partial-completion tracking. The next scheduled invocation is a new run, not a retry. It has no awareness that the previous run failed unless you build that check yourself.
With Render's Cron Jobs, you define a schedule with a standard cron expression and can manually trigger a run via the Trigger Run button in the Render Dashboard. If you manually trigger a run while another is active, Render first cancels the active run, so at most one run of a given cron job is active at a time. Cron jobs are billed like other Render services, prorated by the second, with a minimum charge of $1/month per cron job service. See pricing for the resource model. For a full implementation walkthrough once you've settled on cron, see how Render handles scheduled tasks.
This minimal example shows the scheduled-run shape. It relies on helper functions (previousUtcDay, db, emailReport, buildSummary) that aren't defined here, so treat it as illustrative rather than runnable:
Notice the explicit date boundaries. Computing "the previous UTC day" instead of a rolling 24-hour window means a delayed or manually rerun job still reports on the same, well-defined slice of data. All cron day and time ranges use UTC. For production, add structured logging, alerting on failure, and a check for whether the previous run completed before starting a new one.
Where cron breaks down is long-running work. Render hard-stops every cron run at 12 hours, and if a run is still active when the next scheduled run comes due, the new run is delayed until the active run finishes, not skipped, so a chronically slow job pushes its own schedule.
Before choosing cron, verify that the job has no steps that depend on each other and that "fail today, fix tomorrow" is an acceptable recovery story.
Background workers: bursty, independent units of work
A background worker consumes from a queue, decoupling request and response from processing. Choose this primitive when load is variable and each unit of work is independent. Resizing one image doesn't depend on another image finishing first.
Failure is scoped per message, so one poisoned job doesn't block its neighbors. Retry happens at the queue level, typically with exponential backoff, and exhausted messages land in a dead-letter queue for inspection.
With Render's Background Workers, you get services that run continuously without receiving incoming network traffic. They usually poll a task queue (such as one backed by a Render Key Value instance) and process tasks as they arrive. Because they're a standard service type, you can scale them independently of your web tier, sizing worker capacity to your processing backlog rather than your web request volume. Render does not provide a free instance type for background workers. For a case study of choosing background workers over other async options for a multi-client workload, see how Cynical Sally serves nine clients from one backend on Render.
The hidden prerequisite: queue retries mean at-least-once delivery. Your handler will eventually run twice for the same message. Retries without idempotency don't add safety, they add duplicate emails and double charges. And idempotency checks must be atomic. A "check if done, then do it" sequence still races under concurrent deliveries.
This example illustrates the basic shape of a queue consumer handling one job at a time. It assumes a queue object and a db.results store that aren't defined here:
For production, add dead-letter handling, structured retry backoff, and idempotency keys to prevent duplicate side effects.
Where workers break down is sequential dependencies. If task B needs task A's output and must resume correctly when A retries, you're hand-rolling orchestration in queue handlers. Workers have no native concept of "steps."
Workflows: multi-step processes with resumable state
A multi-step workflow is a sequence of steps whose progress is tracked individually. In the category's ideal form, each step's completion is persisted: if the process crashes after step 2, execution resumes at step 3, and steps 1 and 2 don't re-run. How much of that ideal a given platform provides out of the box varies, so check what your tool actually documents. Either way, this is a distinct category from "a queue with retries," not a fancier version of one.
Two properties define the workflow orchestration category. First, step-level retry is separate from job-level retry: a flaky external API call can retry three times without re-running the expensive LLM call before it. Second, state visibility. Cron and workers give you logs, while workflows give you inspectable, per-step state you can query, resume, and debug.
Render Workflows (currently in beta) provides an all-in-one worker model with managed queuing, automatic retries, and rapid spin-up. On Render, you express steps as chained tasks: each chained task runs in its own instance with its own retry policy, which gives you step-level retry granularity while the parent run is alive. You can set default retry logic, timeout, and instance type for all tasks (and optionally override per task), and track the progress and status of active and completed runs in the Render Dashboard. Render doesn't document checkpoint/resume for the parent task itself: if the parent task fails and retries, its function re-runs from the top, and completed chained runs aren't memoized, so if you need resume-after-crash behavior, make the parent idempotent and checkpoint completed work yourself. Also note a beta limitation: Workflows has no native scheduling yet, and the documented pattern is a cron job that triggers workflow tasks, which is exactly what graduating a cron job's multi-step logic into a workflow looks like in practice. The Render SDK is currently available for TypeScript and Python. For deeper background on the SDK, see Durability as code: Introducing Render Workflows and the trade-off discussion in workflow orchestration platforms for AI agents and LLM workloads. Suspend and resume for use cases like pausing an agent mid-run for approval is a pattern you build on top of these primitives, not a product feature, but it's a good example of behavior that exceeds what cron jobs or simple background workers can handle.
Here's a simplified multi-step workflow definition. It uses a hypothetical workflow/ctx.step API for illustration, so check your workflow tool's SDK for the actual syntax:
This demonstrates the conceptual shape of step persistence, not a complete Render Workflows implementation. Check the current documentation for the actual SDK surface. In the Render SDK, retry settings are configured per task (with maxRetries, waitDurationMs, and backoffScaling), and every run of a task uses the same retry settings. For production, add per-step timeout configuration, compensation logic for partial failures, and monitoring for stuck workflows.
Where workflows break down is trivial single-step jobs, where orchestration is pure overhead, and high-throughput fine-grained tasks, where per-unit orchestration cost outweighs the benefit of per-step tracking and retries.
Mapping failure requirements to primitives
Once you've articulated your failure requirements, the mapping is usually unambiguous.
Primitive | Best for | Failure granularity | Retry behavior | Cost profile | Breaks down when |
|---|---|---|---|---|---|
Cron job | Time-triggered, stateless runs | Whole run | None built-in; next run is a new run | Compute during execution, prorated by the second, plus a $1/month minimum per cron job | Multi-step logic or sub-run retry needs |
Background worker | Bursty, independent units | Per message | Queue-level backoff plus dead-letter queue | Scales with the worker capacity you provision | Sequential step dependencies |
Workflow | Multi-step pipelines with expensive steps | Per step | Step-level policies, distinct from job-level | Orchestration plus state overhead | Trivial jobs or ultra-high-throughput fine-grained work |
Treat the table as a reference, not a replacement for the reasoning above. The "breaks down when" column only makes sense once you can articulate why each seam exists. For the authoritative resource and billing model of each service type, see Render pricing. For broader queue, workflow, and reliability patterns behind these primitives, see infrastructure patterns for agentic applications.
Three worked examples
Nightly report to a cron job. One scheduled run, no cross-step state, and the recovery story is "alert a human, rerun manually or wait until tomorrow." A queue adds nothing because there's no burst to absorb. A workflow adds nothing because there are no steps to resume. The only production hardening you need is failure alerting and explicit date boundaries so reruns produce identical output. The scheduled tasks guide covers the full setup. If you want to see a cron job and full-stack app deployed together, this Hacker News AI agent tutorial walks through both.
Image processing on upload to a background worker. Upload volume is bursty and each image is independent. Per-image retry is exactly the right granularity: one corrupted upload retries and eventually dead-letters without touching the other 10,000. You size worker capacity to match your processing backlog. The mandatory investment is idempotency, an atomic claim per image as shown above, because at-least-once delivery guarantees eventual duplicates.
Multi-step AI pipeline (extract, embed, store, notify) to a workflow. Each step has different failure modes and costs. The embedding call is slow and billed per token, while the database write is cheap and fast. If the store step fails, re-running extraction and embedding wastes money and time. Step-level persistence means a retry resumes at "store," and step-level retry policies let the flaky external call retry aggressively while the LLM step doesn't. This is precisely the seam where workers break down and workflows earn their overhead. For deeper context on the orchestration trade-offs here, see workflow orchestration platforms for AI agents and LLM workloads.
Operational guidance and pitfalls
Do
- Start from failure semantics. Write down what happens at each possible failure point before picking a primitive.
- Make handlers idempotent before enabling retries, using atomic operations (unique constraints, upserts) rather than check-then-write.
- Let a cron job graduate to a worker or workflow when its script grows sequential external calls.
- Use explicit data boundaries (calendar days, checkpoints) in scheduled jobs so reruns are deterministic.
Don't
- Don't treat the next cron tick as a retry. It's a new run with no memory of the failure.
- Don't adopt workflows "just in case." Orchestration overhead on trivial jobs is a real cost.
- Don't assume queue retries are safe by default. They're only safe when the handler is idempotent.
Common mistakes
- The cron script with hidden steps. A "simple" nightly script that calls three APIs in sequence, with no error handling between them, is a workflow wearing a cron costume. Recognize it by asking "what state are we in if call 2 fails?" If nobody can answer, migrate it.
- Assuming retries are idempotent by default. Queues guarantee at-least-once delivery, and nothing about your handler is automatically safe to re-run. Duplicate side effects in production are the symptom.
- Non-atomic idempotency checks. "Query for existing result, then write" races under concurrent deliveries. Use database-level uniqueness to make the claim atomic.
- Confusing job-level and step-level retry while debugging. If a whole pipeline re-ran when only one step failed, you're using job-level retry where you needed step-level retry.
Next steps
Audit one of your existing async jobs against this framework. Write down its failure points, check whether its retry behavior matches its idempotency guarantees, and decide whether it's at a breaking seam. Then read the primitive-specific docs before migrating anything, and test the failure scenarios in your own system rather than trusting any article, including this one.
- Render Cron Jobs: scheduling model and manual triggers
- Render Background Workers: service type overview
- Render Scaling: scaling workers independently
- Render Workflows: step execution and state persistence
- Render Pricing: resource and billing model
- BullMQ documentation: Node.js queue patterns
- Celery documentation: Python task queue patterns
Every snippet in this guide illustrates conceptual patterns (failure isolation, atomic idempotency claims, step persistence) rather than library API surfaces, so adapt each for your specific queue library, framework, or workflow tool. Compensation and rollback capabilities vary by workflow engine, so verify against current documentation before relying on them.