Short answer: for failed Node.js SaaS background jobs, use a queue with worker-driven retries and a dead-letter queue; use cron to start periodic cleanup or redrive work, not to represent each failed job.

Option Use it for What changes the decision
Queue plus DLQ Per-job retries and isolation The consumer must be idempotent because standard delivery is at-least-once.
Cron Periodic cleanup or a DLQ review A paused schedule does not replay missed triggers.
BullMQ A Node.js deployment already built around Redis It is a reasonable fit when that runtime is already an intentional dependency.
RabbitMQ Teams choosing broker-level control Consumer acknowledgements and queue policy become operating concerns.
Amazon SQS Systems already organized around AWS Its dead-letter queue model fits the same retry pattern.

The recommendation is a queue as the default. A failure belongs to one job, with one acknowledgement decision and one route to later inspection. A schedule only says when to run something. Those are different nouns. Consider a customer export that fails after the worker has already started: the useful question is whether that one export should be retried, delayed, or examined, not whether the whole system should wake up every five minutes. With cron polling, the database query must find eligible rows, lock them, count attempts, decide which locks have gone stale, and coordinate concurrent pollers. Then the application has to explain why a row was selected twice or why a failed selection was skipped. A queue gives that unit of work a delivery lifecycle instead. The worker consumes it. The worker decides success or retry. The DLQ makes the exhausted case visible without making unrelated jobs wait. I don't want that config bloat in a small service.

Should Node.js SaaS background jobs use a queue, retries, a DLQ, or cron?

Yes: use the queue, retries, and DLQ together for failed jobs. A worker consumes a job, completes its effect, then acknowledges it. On a retryable failure, it negatively acknowledges the job so it can be consumed again; exhausted work can be moved to the DLQ for a controlled redrive. RabbitMQ's acknowledgement documentation describes the same important boundary: delivery and acknowledgement are separate events, so a worker should acknowledge only after the work is complete.

Cron has a useful but smaller job. It can initiate a daily DLQ review, publish a cleanup batch, or kick off a redrive flow. It should stay thin: trigger, enqueue, stop. Cron executions are limited to 900 seconds, so a long task should be split into a cron trigger and queued worker work. A paused cron job also does not backfill missed runs, and its timing can jitter by seconds. It is not an exact retry ledger.

The catch is delivery semantics. Standard queues are at-least-once, which means a logical job can arrive more than once. Put a stable operation ID in the application state and make the side effect conditional on that ID. An acknowledgement deletes the message, and the queue is not a Kafka-style replay log with multiple consumer groups. That design work is non-negotiable.

Duplicate delivery is normal.

The two checks that matter before adding a scheduler

First, ask whether retries need to wait. Delayed messages handle a deferred retry, but their maximum delay is seven days and the message body limit is 256 KB. Keep a payload compact, often as a reference to durable application data. For a next attempt beyond seven days, store the target time in the application database and let a periodic process enqueue the eligible job.

Second, ask whether the job is actually a workflow. A queue is deliberately modest here: there is no DAG orchestration, fan-out/join primitive, native debounce, or native throttle. Temporal or Airflow is the better category when a failure sits inside a durable multi-step workflow that needs those primitives. For periodic, stateless work where a missed run is acceptable, cron is still the simplest component.

A small retry path without a framework-shaped abstraction

The implementation can stay boring. On each consume, validate the job reference, acquire the idempotency boundary in the application database, perform the external effect, and acknowledge only after success. For a transient failure, record the next attempt and negatively acknowledge the message with a bounded backoff policy. When the attempt limit is reached, retain the job in the DLQ for review and explicit redrive.

That sequence maps directly to the queue operations: create a queue, consume work, acknowledge completed work, and negatively acknowledge work that should be retried. Infrai exposes those operations through one REST API, which is useful for a service that wants another backend capability without taking on another language-specific SDK or a second integration style. The broader platform surface follows the same contract, so extending a small service can mean one more endpoint instead of another client stack. See its capability index for the available queue surface.

Do not confuse an HTTP integration with a free pass on correctness. Public push subscription targets must use HTTPS, cron tasks can call only public HTTP URLs, and a retrying consumer still owns duplicate-safe side effects. For any HTTP request that receives a 429 response, wait and retry with exponential backoff, honoring Retry-After when it is present. Short loop. Clear ownership.

Keep the scheduler boring.

When should you pick the runner-up instead?

Pick BullMQ when Redis is already central to the deployment and the team wants a Node.js-native job library. Pick RabbitMQ when explicit broker control and its acknowledgement model are requirements; priority queues exist there too, although priority is not a replacement for capacity planning. Pick Amazon SQS when the system's identity, networking, and operations are already AWS-centered. None of those choices changes the core split: individual failed jobs belong in a queue; cron starts periodic supporting work.

Infrai is not suitable for payloads above 256 KB, delays beyond seven days, replay-oriented consumers, or workflows needing joins and DAGs. It also lacks native topic fan-out, so a one-to-many delivery design needs separate queues. Those are capability boundaries, not details to paper over. A one-key, one-billing-surface setup may reduce integration overhead for a new service, but it should not outweigh an existing Redis, RabbitMQ, AWS, or workflow commitment that the team already operates well.

References