Job queues and workflow tools for Node.js and TypeScript teams, compared on what they run on, how retries and scheduling work, and what you operate in production.
BullMQ is the default answer for background jobs in Node.js. It is fast, feature-rich and well documented, with delayed jobs, repeatable schedules, priorities, rate limiting and parent-child flows. All of that lives in Redis, which then has to run with settings that are safe for queues.
A BullMQ setup in production means a Redis instance configured for queues, not caching: the BullMQ docs are explicit that maxmemory-policy must be noeviction, or jobs can silently disappear. It also means worker processes you deploy and scale yourself, and a dashboard you add separately to see failed jobs. These are the reasons teams look elsewhere:
Each option is covered below, starting with Encore.
Encore combines an open source infrastructure SDK for TypeScript and Go with a platform that provisions and deploys into your own AWS or GCP account. Instead of a Redis-backed queue, background work is a Pub/Sub topic and subscription in your code. On AWS that becomes SNS topics with SQS queues and dead-letter queues, on GCP Google Cloud Pub/Sub with dead-letter topics, so there is no queue server for you to size or patch.

A BullMQ queue and worker for outgoing webhooks, written with Encore:
// deliveries/deliveries.ts
import { Topic, Subscription } from "encore.dev/pubsub";
import { CronJob } from "encore.dev/cron";
import { api } from "encore.dev/api";
import log from "encore.dev/log";
interface Delivery {
deliveryId: string;
url: string;
payload: string;
}
export const deliveries = new Topic<Delivery>("webhook-deliveries", {
deliveryGuarantee: "at-least-once",
});
// The worker. Failed attempts are retried with backoff, then moved to a dead-letter queue.
const _ = new Subscription(deliveries, "send-webhook", {
handler: async (d) => {
const res = await fetch(d.url, {
method: "POST",
headers: { "Content-Type": "application/json", "Idempotency-Key": d.deliveryId },
body: d.payload,
});
if (!res.ok) throw new Error(`webhook returned ${res.status}`);
},
retryPolicy: { minBackoff: "5s", maxBackoff: "10m", maxRetries: 8 },
});
// The equivalent of a repeatable job.
export const pruneOld = api({}, async (): Promise<void> => {
log.info("pruning old delivery logs");
});
const __ = new CronJob("prune-deliveries", { title: "Prune delivery logs", every: "24h", endpoint: pruneOld });
Run in your terminal to get started locally.
Publishing is await deliveries.publish({...}) from any service. encore run runs the topic and subscription in process, so you can trigger a delivery and watch the retry attempts in the local trace view. Cron jobs do not run locally, but pruneOld is a normal endpoint you can call while developing. On deploy, the worker scales with the service that defines it, and failed messages are kept in the dead-letter queue instead of a Redis list.
encore run. The same declaration is used in every cloud environment.What you do not get is BullMQ's job-level features such as priorities, per-queue rate limits or parent-child flows. If those drive your design, Temporal or BullMQ itself remain better fits.
pg-boss stores jobs in PostgreSQL and relies on SKIP LOCKED so concurrent workers can claim jobs without blocking each other. It supports retries with exponential backoff, priority queues, dead-letter queues with redrive, cron scheduling, job dependencies and creating jobs inside an existing database transaction, all inside your existing database and its backups.
It is the shortest path off Redis for a Node app that already runs Postgres. The trade-offs are throughput and load: every job is rows written and updated in your primary database, so very high job volumes compete with application queries, and workers are still processes you deploy yourself.
Graphile Worker is another PostgreSQL job queue for Node.js, built by the PostGraphile team. Jobs can be added from SQL as well as from JavaScript, so a database trigger can enqueue work, and workers are notified of new jobs through Postgres LISTEN/NOTIFY for low latency.
It suits teams that want jobs created inside transactions or from the database itself. Like pg-boss, it shares capacity with your primary database, and scheduling, retries and job inspection are all things you work with through its API and tables rather than a hosted dashboard.
Inngest replaces queues with durable functions. You send events, and functions made of step.run calls react to them, with each step retried and its result stored, so a function can sleep for days or wait for another event without holding a worker. It runs on Inngest's platform or on a self-hosted Inngest server, and it calls your functions over HTTP, which fits serverless deployments.
In exchange you take on a new execution model and a dependency on Inngest's runtime: steps must be written with replay in mind, and your job state lives in Inngest, not your database.
Trigger.dev runs TypeScript tasks on its own infrastructure, or on yours when self-hosted, with no execution timeouts. Tasks get retries, scheduling, concurrency controls and a dashboard with logs per run, which makes it popular for AI pipelines, media processing and other work that outgrows serverless limits.
Your task code is deployed to Trigger.dev separately from your API, so you manage two deploy targets, and data your tasks need has to be reachable from where they run.
Temporal is a workflow engine. You write workflows as ordinary TypeScript or Go functions, Temporal records every step in an event history, and a workflow resumes exactly where it was after crashes, deploys or months of waiting. It is the strongest option for sagas, approvals and multi-service processes where correctness matters more than simplicity.
It is also the most to adopt, since you run a Temporal cluster or pay for Temporal Cloud, workflow code has determinism rules, and simple fire-and-forget jobs carry more ceremony than in a plain queue.
Pick Encore when background jobs are part of a backend you want to run in your own AWS or GCP account, with queues, retries, dead letters and cron declared next to the code. Pick pg-boss or Graphile Worker to drop Redis while staying on a library and your own Postgres. Pick Inngest or Trigger.dev for hosted durable or long-running tasks, and Temporal for workflows where every step must survive failure.
Encore if you want queues declared in TypeScript code and provisioned as SNS and SQS or Google Pub/Sub in your own cloud. pg-boss or Graphile Worker if you want a job queue on the Postgres you already run. Inngest or Trigger.dev for hosted durable functions and long-running tasks. Temporal for complex, long-lived workflows.
Yes. pg-boss and Graphile Worker store jobs in PostgreSQL and use row locking so several workers can share a queue. Managed queues such as Amazon SQS or Google Pub/Sub are another option, with the queue run by the cloud provider instead of a Redis instance you operate.
BullMQ stores jobs and queue state in Redis keys. If Redis evicts keys under memory pressure, jobs and their state can disappear, so the BullMQ docs say noeviction is the only setting that guarantees correct queue behavior. That rules out many shared or cache-tuned Redis instances.
Deploy to your cloud on AWS and Google Cloud
without managing infrastructure
Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.