pg-boss vs BullMQ in 2026: Which Node.js Job Queue?
pg-boss vs BullMQ compared: scheduling, retries, rate limits, transactions and throughput, now that BullMQ 6 also runs on PostgreSQL. Plus a decision checklist.
pg-boss is a Node.js job queue built only on PostgreSQL, with jobs you can create inside your own database transactions, cron and RRULE schedules, and queue policies for throttling. BullMQ runs on Redis by default and, since version 6, on PostgreSQL too, with a global rate limiter and SDKs in six languages. Already on Postgres with a Node app? Start with pg-boss.
The short version of pg-boss vs BullMQ: pick pg-boss when PostgreSQL is the only datastore you want to run and your workers are Node.js. Pick BullMQ when you need a requests-per-second rate limiter, workers in languages other than JavaScript, or the higher throughput of Redis. Both are open source, both deliver jobs at least once, and both handle delayed jobs, cron schedules, retries with exponential backoff and job dependencies.
The old framing of this comparison, "Postgres queue or Redis queue", stopped being accurate on July 30, 2026, when BullMQ 6.0 shipped a PostgreSQL backend. So the real question is now which library fits your stack and your workload, and that is what this guide compares, using the current docs for pg-boss 12 and BullMQ 6. We focus on the part most comparisons skip: jobs that must run at a specific time, like a scheduled social media post. For the design underneath any queue, our guide to job queue architecture for scheduled posts covers claiming, leases and recovery.
PostGate handles OAuth, publishing and delivery evidence for every network through one API. Start free or read the docs.
What is the difference between pg-boss and BullMQ?
pg-boss is a library that stores jobs in tables inside your existing PostgreSQL database. According to its introduction, workers claim jobs with Postgres's SKIP LOCKED, so many workers can share a queue without blocking each other. It needs Node 22.12 or newer (or Bun) and PostgreSQL 13 or newer, and it is MIT licensed and maintained by one person.
BullMQ is a job queue with official SDKs for TypeScript/JavaScript, Python, Elixir, Rust, .NET and PHP. Redis (or a compatible server) has always been its home. The 6.x line added a PostgreSQL backend that runs the same Queue and Worker API. Here is how the two compare today:
| Feature | pg-boss 12 | BullMQ 6 |
|---|---|---|
| Storage | PostgreSQL 13+ only (plus CockroachDB, Citus, PGlite) | Redis by default, or PostgreSQL 13+ |
| Runtimes | Node.js 22.12+ or Bun; SQL for most operations elsewhere | SDKs for six languages |
| Enqueue in your transaction | Yes, with adapters for Drizzle, Knex, Kysely and Prisma | Not documented in the Postgres guide |
| Delayed jobs | startAfter: seconds, ISO date, Date or "1 hour" | delay in milliseconds |
| Recurring jobs | Cron or RRULE with time zone, minute resolution | Job Schedulers: cron (with seconds) or fixed interval, time zone |
| Rate limiting | Throttling slots, queue policies, group concurrency | Global limiter (max jobs per duration), manual limit on 429 |
| Failed jobs | Dead letter queues with redrive | Failed set you can inspect and retry |
Does BullMQ on Postgres make pg-boss unnecessary?
Not yet, though it narrows the gap. The BullMQ PostgreSQL guide says the backend implements the full API (flows, job schedulers, rate limiting, priorities, delayed jobs, deduplication, metrics and events), so code written for Redis runs unchanged. It also says Redis "remains the default and the most battle-tested option". The Postgres backend keeps its objects in a bullmq schema, uses one long-lived LISTEN connection per backend to wake workers, and does not migrate itself: you call runMigrations from a deploy step.
Throughput is the honest trade-off. BullMQ's own guide puts processing on Postgres at roughly 1.5 to 2 times fewer jobs per second than on Redis on similar setups. Its rough laptop numbers show about 11,000 jobs per second on Postgres against 18,000 on Redis at concurrency 8 to 32, with trivial jobs. pg-boss does not publish an equivalent benchmark, so if throughput decides it, test both with your real payloads.
What pg-boss still does that BullMQ's guide does not cover is sharing a transaction with your data. You can create the job in the same transaction as the row it belongs to, so the job exists if and only if the change commits. A transactional worker can also commit its own writes together with the job's completion. For a scheduler, that means "save the post" and "queue the publish job" can never get out of sync.
How do delayed and cron jobs compare?
For one-off jobs, pg-boss is easier to read. startAfter takes a number of seconds, a Date, a relative string like '1 hour' or an ISO 8601 date time. Per the jobs API, a string with Z or an offset resolves to that exact instant, and one without a zone is read as UTC, never the database's time zone. While the job is still queued, update() can change its startAfter, which is how you handle a user moving a post.
BullMQ takes a delay in milliseconds, so a fixed publish time means computing the difference from now. Its delayed jobs guide adds that the exact moment is not guaranteed when workers are busy, though it is accurate in practice.
For recurring jobs, the libraries differ in resolution. pg-boss checks schedules every 30 seconds and files each job under the minute its occurrence falls in, so a schedule sends at most one job a minute, and at least one pg-boss instance must be running. In return it accepts RRULE expressions next to cron, which cover cases cron cannot express, like "5pm on the last Friday of the month". Its missed: 'once' option sends one catch-up job after an outage. The default skips anything that came due while nothing was running.
BullMQ's Job Schedulers, created with upsertJobScheduler, accept a cron pattern with an optional seconds field or a fixed interval, plus a time zone. One detail from its docs: a scheduler produces the next job only when the previous one starts processing, so a backed-up queue gets its recurring jobs less often than the pattern says.
import { PgBoss } from 'pg-boss';
const boss = new PgBoss(process.env.DATABASE_URL!);
boss.on('error', console.error);
await boss.start();
// The dead letter queue has to exist before another queue points at it
await boss.createQueue('publish-failed');
await boss.createQueue('publish', {
retryLimit: 5,
retryDelay: 30, // seconds
retryBackoff: true,
deadLetter: 'publish-failed',
});
// One-off job: publish at a fixed instant (ISO string with offset, or a Date)
const jobId = await boss.send(
'publish',
{ postId: 'post_123' },
{ startAfter: '2026-11-20T14:00:00Z' },
);
// The user moved the post: change the start time while the job is still queued
await boss.update('publish', undefined, {
id: jobId!,
startAfter: '2026-11-21T09:30:00-05:00',
});
// Recurring job: Mondays at 09:00 in New York (minute resolution)
await boss.createQueue('weekly-digest');
await boss.schedule('weekly-digest', '0 9 * * 1', { feed: 'weekly' }, { tz: 'America/New_York' });
await boss.work('publish', async ([job]) => publish(job.data));How do retries, duplicates and rate limits compare?
Retries look alike. pg-boss defaults to 2 retries per job and supports a fixed delay or exponential backoff with jitter, capped by retryDelayMax. A job that runs out of retries can be copied to a dead letter queue and redriven later. BullMQ uses attempts with fixed, exponential or custom backoff and keeps exhausted jobs in a failed set.
Duplicates and pacing are where the designs split. pg-boss uses queue policies: singleton allows one active job, stately one job per state, and key_strict_fifo runs jobs that share a singletonKey in order, one at a time. Throttling (sendThrottled, sendDebounced) accepts one job per time slot per key, and groupConcurrency caps how many jobs from one group run at once across all nodes. What pg-boss lacks is a plain "N jobs per second" limiter.
BullMQ has exactly that. Its rate limiter is global, so ten workers set to 10 jobs per second still process 10 per second in total. When an API answers 429, a worker can call worker.rateLimit(ms) and throw Worker.RateLimitError() to pause the queue without counting a failure. If your workers call rate-limited social APIs, that is a real point for BullMQ. Our BullMQ vs RabbitMQ comparison covers the limiter and deduplication by jobId in more detail.
Which should you use for scheduled social media posts?
Scheduled posts mix every hard part of a job queue: a publish time weeks away that users keep editing, recurring slots in the user's time zone, retries against APIs that fail in odd ways, and per-network rate limits. Use this checklist:
- Node app already on Postgres, modest volume: pg-boss. One database to back up, jobs created in the same transaction as the post, and
update()for reschedules. - Workers in Python, PHP or .NET, or many calls against strict API quotas: BullMQ, on Redis or on its Postgres backend if you want to skip Redis.
- Very high throughput: BullMQ on Redis, and benchmark before you commit.
- Posts scheduled far ahead: with either library, keep the post row as the source of truth and treat the job as a pointer to it. pg-boss counts its default 14-day retention for queued jobs from
startAfter, so a job scheduled for next month is not deleted while it waits. - Every case: both deliver at least once, so the publish call needs an idempotency key or a crash at the wrong moment posts twice.
The queue is also the smaller part of the work. OAuth refresh, media processing and each network's error codes take longer to get right, as our breakdown of how social media schedulers work shows. If publishing is one feature of your product rather than the product itself, PostGate's schedules API takes cron expressions or daily slots with an IANA time zone and reports delivery for each platform target, so your pg-boss or BullMQ worker only has to make one API call.
Frequently asked questions
Is pg-boss better than BullMQ?
For a Node.js app that already runs PostgreSQL and wants jobs created inside its own transactions, pg-boss is usually the simpler choice. BullMQ is better when you need a global requests-per-second limiter, workers in other languages or Redis-level throughput.
Can BullMQ use PostgreSQL instead of Redis?
Yes. BullMQ 6.0, released on July 30, 2026, added a PostgreSQL backend that needs PostgreSQL 13 or newer and the pg package. Its docs say Redis remains the default and that processing on Postgres is usually around 1.5 to 2 times slower.
Does pg-boss support cron jobs?
Yes. boss.schedule() takes a cron expression or an RRULE plus a time zone. Schedules are checked every 30 seconds with minute resolution, and at least one pg-boss instance has to be running for them to fire.
Does pg-boss need Redis?
No. pg-boss stores everything in PostgreSQL 13 or newer and creates its own tables in a separate schema on the first start(). It runs on Node.js 22.12 or newer, or on Bun.
