Somanath StudioTalk to an Engineer
Back to Writing
12 min read
SaaS background jobsjob queue architecturedurable workflowscron jobs

Cron, Queue, or Workflow? A SaaS Background Jobs Guide

A SaaS request routing background work to cron, queue and durable workflow execution paths

Many SaaS background jobs begin as a few extra lines inside an API route.

A user requests an export. The route queries data, builds a file, uploads it and sends an email before returning. It works in development. Then the dataset grows, an external API slows down, a deployment interrupts the process, or two retries create two invoices.

The problem is not simply that the job became “too slow.” The code now has a lifecycle that no longer matches an HTTP request.

The usual response is to add infrastructure: perhaps a cron schedule, a queue or a durable workflow engine. That can improve reliability, but only when the execution model matches the business operation. Choosing the most sophisticated option by default can produce more state, cost and operational surface than a small team needs.

This trade-off is especially visible now. Cloudflare began billing Workflows for persisted steps and storage on August 10, 2026, in addition to requests and CPU time. Its current pricing documentation makes each durable boundary an architectural and economic choice, not just a code-organization preference.

For a founder or small engineering team, the right question is not “Which background-job product should we install?” It is “What must happen, what may fail, and how do we prove the work reached a safe outcome?”

First Decide Whether the Work Should Be Background Work

Moving code out of a request adds a boundary. The caller no longer receives the final result immediately, so the product needs a job identifier, status and a way to communicate completion.

Keep work in the request when it is fast, bounded and necessary to produce the response. Validating input, enforcing authorization and creating the database record usually belong there. A small query that completes well within the platform timeout may belong there too.

Move work out when one or more of these conditions apply:

  • it calls a slow or rate-limited external service;
  • its duration grows with customer data;
  • the user does not need the final result immediately;
  • failure should be retried after the request has ended;
  • traffic bursts would overload a dependency;
  • it waits for a time, event or human approval;
  • it contains several steps that need separate recovery rules.

The request should normally commit the user’s intent and return a stable identifier. For an export, that might mean creating an export_job row with status queued and returning 202 Accepted with the job ID. The product can then show progress, poll a status endpoint or notify the user later.

This boundary also keeps the core application closer to the boring architecture that makes SaaS easier to operate: the web tier handles interactive traffic, while a deliberately chosen executor handles deferred work.

Use Cron for Time, Not for Unowned Work

Cron answers one question well: “When should a process start?”

It is appropriate for bounded, repeatable scans such as expiring stale invitations, creating a daily summary, scheduling renewals or finding jobs that need reconciliation. The schedule is the trigger; the database remains the source of truth.

Cron is a poor substitute for a queue when every user action creates independent work. A job that wakes every minute, loads all unprocessed rows and processes them in one long loop creates hidden queue semantics without the controls of a queue. A large tenant can block smaller tenants, retries replay the whole batch, and the backlog becomes a database query rather than an observable operational object.

Scheduling also does not imply exactly-once execution. Vercel’s cron management guidance says failed invocations are not retried, overlapping invocations can occur, and the same cron event can occasionally be delivered more than once. Those are not unusual edge cases to “fix later.” They define the correctness model.

A robust cron task should therefore:

  • acquire a bounded lease or select rows with concurrency-safe locking;
  • process a limited batch;
  • make each item idempotent;
  • record a checkpoint or next-run cursor;
  • stop before its runtime limit;
  • expose backlog age and failure count.

Cron can also act as a safety net. A frequent queue consumer handles normal work, while a slower scheduled reconciler finds records stuck between states. That is a better use of cron than making it the only delivery mechanism for every background action.

Use a Queue for Independent, Repeatable Units of Work

A queue fits work that can be expressed as independent messages and processed by interchangeable workers.

Examples include generating one export, resizing one uploaded image, delivering one webhook, indexing one document or sending one notification. The queue absorbs bursts, lets workers consume at a controlled rate and separates request availability from downstream capacity.

For many SaaS products, this is the best first step beyond a request handler. It adds fewer concepts than a workflow engine while solving the central problem: reliable, asynchronous handoff.

But a queue is not a correctness guarantee. Amazon SQS documents that standard queues use at-least-once delivery, so a consumer can receive the same message again. Network ambiguity creates the same problem with other systems: the worker may complete a side effect, lose the acknowledgement, and receive the job again.

Every message should carry a stable identity and enough context to find current state without embedding a sensitive database snapshot:

type ExportJobMessage = {
  eventId: string;
  jobId: string;
  tenantId: string;
  requestedBy: string;
  schemaVersion: 1;
};

The consumer can claim eventId with a unique database constraint before doing irreversible work. For an external API, pass the same idempotency key on every attempt when the provider supports it. For an email, record the delivery attempt and define whether a duplicate is acceptable or must be suppressed.

Retry only errors likely to recover. A timeout or 503 may deserve exponential backoff. Invalid input, a revoked tenant or a permanent 404 usually does not. After a bounded number of attempts, isolate the message for inspection. A dead-letter queue that nobody monitors is only a quieter way to lose work.

Use a Durable Workflow for Coordinated State Over Time

A durable workflow fits a business process with multiple stages, waits or compensation rules.

Consider onboarding an enterprise tenant:

  1. provision an isolated workspace;
  2. wait for domain verification;
  3. create default roles;
  4. import data from another system;
  5. pause for an administrator’s approval;
  6. enable billing and notify the customer.

A queue can run every step, but your application must persist the state machine, schedule waits, correlate replies and decide which step to resume. A workflow engine moves much of that coordination into the execution model.

Cloudflare describes durable execution as persisting state so a program can resume without the application manually serializing its position. Its August 25 Workflows guide supports automatic retries, hours- or days-long execution and coordination across third-party APIs.

That makes a workflow a good fit when the process:

  • has several steps with different retry policies;
  • waits beyond a normal compute lifetime;
  • pauses for a webhook, event or human decision;
  • needs visible per-instance progress;
  • must resume from the last successful boundary;
  • requires compensation when a later action fails.

Do not use a workflow merely because a function has three helper methods. Durable steps persist state and incur operational meaning. If 100,000 jobs each contain ten unnecessary steps, the system has one million billable and observable boundaries to understand.

Durability also does not create exactly-once side effects. Cloudflare’s own rules for workflow steps warn that a step may retry and recommend idempotent external calls. The engine may remember that a step completed, but it cannot atomically commit a change inside an unrelated payment, email or CRM system.

The right step boundary is usually a meaningful unit of recovery: “create export record,” “render file,” “store artifact,” or “send completion notice.” If rendering fails, the workflow should not repeat the completed data query unnecessarily. If the notification fails, it should not regenerate the file.

The Database-to-Queue Gap Is the Failure Most Teams Miss

Suppose the request commits an export record and then publishes a queue message.

If the database commit succeeds but publishing fails, the user sees a queued export that no worker will receive. Reverse the order and a fast worker may process a message for data that later rolls back.

This is the dual-write problem. Adding a managed queue does not remove it.

The transactional outbox pattern writes the business record and an outbox event in the same database transaction. A relay publishes committed outbox rows, then marks them delivered. AWS’s prescriptive guidance for transactional outboxes recommends this pattern when a database update must initiate an event and also notes that consumers still need duplicate protection.

For a small SaaS, the implementation can remain modest:

BEGIN
  INSERT export_jobs (..., status = 'queued')
  INSERT outbox_events (event_id, type, payload, created_at)
COMMIT

A scheduled or continuously running relay publishes unsent outbox rows. The queue worker claims each event_id once, performs the work and advances the job state. A reconciler flags old queued or running records.

This is more plumbing than calling queue.send() directly, so apply it where lost handoffs matter. Billing, fulfillment, customer exports and lifecycle automation usually justify it. A disposable cache refresh may not.

Choose the Smallest Model That Matches the Failure

Use this decision table before choosing a vendor or library:

| Requirement | Request | Cron | Queue | Durable workflow | | --- | --- | --- | --- | --- | | User needs immediate result | Best fit | Poor fit | Poor fit | Poor fit | | Starts at a known time | Manual | Best fit | Triggered by scheduler | Good when followed by stages | | Absorbs traffic bursts | No | Limited | Best fit | Good, with platform limits | | Independent repeatable jobs | Limited | Awkward at volume | Best fit | Often unnecessary | | Multi-step recovery | Manual | Manual | Application-managed | Best fit | | Waits for event or approval | No | Polling | Application-managed | Best fit | | Lowest conceptual overhead | Best fit | Good | Moderate | Highest |

One product may use all four without becoming a distributed-systems science project.

An API request creates an export record. A queue generates the file. A nightly cron reconciles stuck jobs. A durable workflow handles a separate enterprise import that pauses for approval and coordinates several external services.

The mistake is not mixing primitives. It is giving one primitive responsibilities it cannot safely express.

Design the Job Contract Before the Worker

Whichever executor you choose, define a job as a business object rather than an invisible function call.

Store at least:

  • a globally unique job or event ID;
  • tenant and actor context;
  • job type and payload schema version;
  • current state and attempt count;
  • creation, start and completion timestamps;
  • a safe error code and operator-facing diagnostic reference;
  • a link to the resulting artifact or business record;
  • retention and cancellation rules.

Do not trust tenant context copied from a browser request without reauthorizing it at execution time. Permissions and resource ownership can change while a job waits. Derive authority from trusted server-side state and scope every query to the tenant.

Also decide what the customer sees. “Processing” forever is not a status model. Use explicit states such as queued, running, waiting, succeeded, failed and cancelled, with product copy that explains the next action.

These records become the correlation spine for the minimum viable observability a SaaS needs. Useful signals include queue age, completion latency, retry rate, permanent-failure rate and the oldest job per tenant—not merely worker CPU.

Common Failure Modes to Remove Early

Returning success before intent is durable

If the API returns 202 before a job record or outbox event is committed, a process crash can erase accepted work. Persist intent first.

Retrying the entire batch

A daily job that processes 10,000 customers and fails at customer 9,900 should not replay the first 9,899 side effects. Checkpoint per item or enqueue independent units.

Treating a lock as idempotency

A lock reduces concurrent execution. It does not protect against a retry after the lock expires or after an ambiguous external response. Use stable operation IDs and recorded outcomes too.

Hiding poison jobs

Infinite retries turn one malformed message into noise and cost. Bound retries, classify permanent failures and make dead-letter work visible to an owner.

Putting large payloads in the executor

Store large files and sensitive snapshots in the appropriate data store. Pass identifiers and versions through the queue or workflow so retention, access and deletion policies remain clear.

Adding a workflow before defining compensation

A state diagram is not a recovery policy. If step four fails after steps one to three changed external systems, decide whether to retry, pause for an operator or compensate earlier actions.

These are part of making a SaaS MVP production-ready, even when the first implementation uses one database and one worker process.

A Seven-Step Background Jobs Action Plan

  1. Inventory deferred work. List every API route, cron handler and webhook that performs slow or retryable work. Record duration, frequency, business impact and downstream dependencies.

  2. Name the required outcome. For each job, define the committed business state. “Run the function” is not an outcome; “one downloadable export exists for this request” is.

  3. Classify the trigger and lifecycle. Choose request for immediate bounded work, cron for time, queue for independent units and workflow for coordinated state over time.

  4. Add stable identity and idempotency. Give each operation an ID, enforce uniqueness at the database boundary and pass the ID to external systems that support idempotency keys.

  5. Close the handoff gap. Use a transactional outbox or a platform-specific atomic mechanism when losing the database-to-executor handoff would affect money, customer data or contractual work.

  6. Define recovery before scaling workers. Set retry classes, backoff, timeout, dead-letter, replay, cancellation and compensation behavior. Assign an owner for failed work.

  7. Measure outcomes and cost. Track backlog age, completion latency, duplicate suppression, permanent failures and per-job execution cost. Test worker crashes and duplicate delivery in staging before increasing concurrency.

If that review exposes unclear ownership across request handling, database state and background execution, a focused production-readiness upgrade is usually cheaper than debugging lost or duplicated customer work after launch.

Reliability Comes From Explicit Outcomes

Cron, queues and durable workflows are not competing maturity levels.

Cron is a scheduler. A queue is a buffer and delivery mechanism. A durable workflow is a persisted coordinator. Each solves a different lifecycle, and each still needs business-level idempotency, trusted tenant context, observable states and a recovery path.

Start with the smallest execution model that makes the failure explicit. Persist the user’s intent, design every side effect for repetition, and make unfinished work visible. That is what turns a background function into production architecture.

Working on a SaaS that's starting to feel fragile?

Talk to an engineer about the parts that break first — without rewriting what already works. We'll recommend focused support or a compact team based on your scope.

Talk to an Engineer