Migrating production infrastructure? Get up to $10K in migration credits.

Apply now

Serverless functions vs workflows: where long-running tasks should live

The failure usually looks like this: a serverless function orchestrating an AI agent loop or a video transcoding job dies at the platform's execution ceiling, halfway through step three of five. The developer's fix is a chain of queue messages, a state table, and a polling function that re-invokes itself every 30 seconds, and now the architecture has more moving parts than the feature it supports.

The instinct is to blame the timeout, but the timeout is a symptom. The root cause is that serverless functions were never designed to hold state across time, and multi-step work is stateful.

This article gives you a mental model for that boundary. It covers why stateless primitives break down for long-running work, what durable execution guarantees, a decision table for matching workloads to primitives, and how to move a single painful path without a full migration. Serverless isn't broken. It's the wrong primitive for one class of work, and this article is about drawing that line.

Why serverless breaks down for long-running work

Serverless platforms achieve their economics by multiplexing many short-lived invocations across shared infrastructure. Every design constraint that frustrates long-running work flows from that model. None of them are arbitrary.

Execution timeouts exist because of resource multiplexing. Platforms impose hard ceilings (AWS Lambda caps invocations at 15 minutes, and edge function platforms are often far stricter) because an invocation that runs indefinitely can't be scheduled, billed, or reclaimed efficiently. The ceiling is what makes per-invocation pricing possible.

Statelessness means every invocation starts from zero. A function has no memory of prior invocations. For a request/response handler, that's a feature: horizontal scaling with no coordination. For a five-step pipeline, it means the function itself can't answer the question "where was I?" That state must live somewhere external.

External state becomes an architecture of its own. To track progress across invocations, teams assemble a queue for hand-offs, a database or Redis for checkpoint state, a cron poller or self-invoking function for status checks, and manual retry-with-backoff logic wired between them. Each component is individually reasonable. Together they form a distributed system whose only job is simulating a call stack.

Cold starts compound across chained invocations. One cold start on a user-facing endpoint is a latency blip. A pipeline that chains six invocations through a queue can pay that penalty six times, and the polling loops between steps add both latency and invocation cost.

The result is an architectural tax. You pay in code, infrastructure, and failure modes to force a stateless primitive to solve a stateful problem. For the general case against serverless, including cost and vendor-coupling considerations, see when to avoid using serverless functions. Here we focus specifically on the long-running-task class of problem.

What "durable execution" means

Durable execution is something the runtime does for you. The execution engine tracks each task run in a multi-step workflow so that when a step fails, only that step is retried, not the entire workflow. Four primitives make this concrete:

  • Steps are units of work with defined boundaries. The boundary tells the runtime that everything inside it either completes or gets retried as a unit.
  • Task runs are how steps execute. Each chained task runs in its own instance, and the runtime tracks each run independently.
  • Retries re-execute only the failed task run. A subtask that fails is retried on its own while the parent run is alive, so a failure in a later task doesn't re-run the tasks that completed before it.
  • Idempotency remains your responsibility. The runtime guarantees a failed step will be retried, but it cannot guarantee the step is safe to retry. A step that charges a card must check whether the charge already exists.

In Render Workflows, the unit is a task: a standard TypeScript or Python function that you register with Render using the SDK. A task can chain runs of other tasks (or itself). Task runs time out after 2 hours by default, and the timeout is configurable per task from 30 seconds up to 24 hours. The runtime retains each task run's input arguments and return value (its task state) for 30 days to support retries, debugging, and observability, and the total arguments passed to a single task run cannot exceed 4 MB. Pass large artifacts by reference (an object-store key, a database ID) rather than by value.

If you want a deeper tour of the orchestration space and how durable execution compares across platforms, see durable workflow platforms for AI agents and LLM workloads.

The contrast with the DIY stack is direct:

Concern
DIY on serverless
Workflow runtime
Progress tracking
State table you design and migrate
Tracked automatically by the runtime per task run
Step hand-off
Queue + message schema
Function call between tasks
Failure recovery
Cron poller + manual backoff logic
Automatic retry of the failed step only
Observability
Stitched from logs across services
Per-run state retained by the platform

Decision table: matching workload to primitive

Use the table to recognize your workload's shape. Read each row by asking two questions about your own workload: does it complete within one invocation, and does it ever need to resume after a failure? Those two properties (duration shape and resume requirement) determine the primitive far more reliably than team preference or existing infrastructure.

Workload type
Typical duration
State needs
Recommended primitive
Why
API handler (request/response)
Milliseconds–seconds
None between requests
Completes in one invocation, so statelessness costs nothing here.
Webhook receiver
Milliseconds
Acknowledge, then hand off
Web service that acknowledges and triggers a workflow task
The receiver's only job is fast acknowledgment. Heavy follow-up belongs elsewhere.
Scheduled/cron job
Seconds–minutes
Single-shot, no resume
Time-triggered, self-contained work doesn't need external progress tracking.
Multi-step pipeline (ETL, transcoding)
Minutes–hours
Progress across steps
Workflow
Task boundaries and per-task retries replace the queue-and-poller stack.
AI agent loop (multi-turn, tool calls)
Unpredictable
Conversation and tool state across turns
Workflow
Unpredictable duration plus per-step failure (model timeouts, tool errors) demands resumability.
Long compute (batch LLM jobs, data processing)
Minutes–hours
Partial results worth preserving
Workflow or background worker
Losing hours of progress to one transient failure is the exact cost that retrying only the failed task run avoids.

For teams deciding specifically among scheduling and background primitives, the comparison of cron jobs vs background workers vs workflows covers that boundary in depth. The short version: workflows earn their place when the work has internal steps that fail independently. Length alone doesn't qualify.

For the agent-loop and batch-LLM rows specifically, infrastructure patterns for agentic applications explains why these stateful workloads need a different pattern than traditional serverless.

A minimal multi-step workflow example

To make the primitives concrete, consider a report generator with three steps: fetch source data, summarize it, and publish the result. In the queue-and-poller version from earlier, each arrow between steps is a queue message, and each retry re-enters the pipeline from a state table lookup. In the workflow version, each step is a task.

In Render Workflows, tasks are defined with the task helper (importing TaskContext) and chained runs are dispatched through ctx.run(...):

The structure is the point. A failure inside publishReport retries only that task run while the parent generateReport run is alive, so fetchSource is not re-run. Throwing on a non-OK response is what makes the failure visible to the runtime's retry machinery.

When serverless is still the right call

Nothing above argues against serverless. It argues against using serverless for workloads shaped like pipelines. For workloads shaped like requests, serverless remains the correct default:

  • Fast API endpoints. Sub-second request/response work benefits directly from stateless scale-out.
  • Webhook receivers. Acknowledge fast, enqueue or trigger downstream work, return. The receiver itself has no multi-step state.
  • Cost-sensitive bursty traffic. Scale-to-zero economics are hard to beat for spiky, short-duration load.
  • Simple transformations. Image resizing, payload reshaping, single-call proxying: one invocation, one result.

The conceptual test cuts both ways. Wrapping a 200ms API handler in a workflow is the same category of error as running an agent loop on a function: a primitive mismatched to the workload's shape. Most production systems that adopt workflows keep the majority of their surface area on functions.

Migrating one path, not everything

The realistic scenario is a team with twenty serverless functions where exactly one (an agent loop, a report generator, a nightly ETL job) is generating timeout alerts and polling hacks. The pattern is surgical:

  1. Identify the workload matching the long-running rows of the decision table: multiple steps, unpredictable duration, or a resume-after-failure requirement.
  2. Define task boundaries where the DIY version currently writes checkpoint state or enqueues a message. Those seams are your step boundaries, already discovered by the pain.
  3. Register the tasks and trigger the workflow from the same event that invoked the old function chain.
  4. Cut over that one path (behind a feature flag if the workload is critical), keep both primitives talking to the same data layer, and retire the queue, state table, and poller once the workflow is stable.

The other nineteen functions don't move. Expect the cutover itself to take days, not weeks. The hard design work already happened when the DIY version's queue schema and state table defined your step boundaries. Registering tasks is comparatively mechanical.

This is what step 2 looks like on a real pipeline. Render's own data team ran its dbt pipeline twice a day, plus emergency reruns, because an upstream invoice projection job landed at unpredictable times, and observability was spread across Render, BigQuery, Stitch, and dbt logs. Rebuilt as a workflow, the projection-readiness check became a task that uses the retry settings (max_retries, wait_duration_ms, and a backoff_scaling of 1) as a "wait until ready" primitive instead of a poller, dependent models run only when the data is there, and four downstream tasks (docs sync, cost monitoring, Metabase cache warming, and daily reports) run in parallel. The task boundaries fell exactly where the old version's polling and rerun logic lived. The result: about $30K/year saved on redundant dbt runs and $70K/year from the cache warming, with task timings recorded to a workflow_run_events table.

Common mistakes

  • Treating steps as if they share memory. Each task run is an independent execution. State crosses step boundaries only through explicit inputs and outputs.
  • Assuming retries are safe by default. A step that charges a card or sends an email will re-execute on retry. Non-idempotent side effects need existence checks or idempotency keys.
  • Migrating everything. Moving fast request/response paths onto workflows trades one mismatch for another. Move the painful path only.
  • Believing workflows eliminate failure thinking. Durable execution removes the plumbing (queues, pollers, state tables), not the need for idempotent design and compensation logic.

The shape of the workload decides

The mental model that outlasts this article: primitives have shapes, and so do workloads. Stateless, single-invocation work belongs on functions. Multi-step, resumable work belongs on workflows. The job that died at step three didn't have a timeout problem. It had a stateful workload on a stateless primitive. Categorize by invocation boundaries and resume requirements, and the decision makes itself, including for workloads no table anticipated. For current limits and SDK syntax, start with the Render Workflows documentation.

Frequently asked questions