Sidequest vs BullMQ
Both run background jobs in Node.js. The real difference is where the jobs live: your existing database instead of a dedicated Redis instance.
The core difference
BullMQ adds a service
BullMQ is fast and battle-tested, but it requires Redis. That means a second stateful service to provision, pay for, back up, secure, and monitor, separate from the database where your data already lives. Jobs and data can drift apart (a job for a row that was never committed, or a missing job for one that was).
Sidequest reuses your database
Sidequest stores jobs in the Postgres, MySQL, SQLite, or MongoDB you already run. The queue is just a table. One fewer moving part, one fewer bill, and you can inspect failures with the same SQL client you use for everything else. It is the same thesis as Solid Queue (Rails) and Oban (Elixir).
Side by side
| Sidequest | BullMQ | |
|---|---|---|
| Datastore | Your existing SQL / document DB | A dedicated Redis instance |
| Extra infrastructure | None; reuse your app database | Redis to provision, secure, and monitor |
| Durability | Persisted to disk by your DB | In-memory, with RDB/AOF persistence options |
| Where jobs live | Same database as your data (no split-brain) | A store separate from your data |
| Backends | Postgres, MySQL, SQLite, MongoDB | Redis only |
| Dashboard | Built in | Separate package (e.g. Bull Board) |
| TypeScript | Native, first-class | Native, first-class |
| Inspect with SQL | Yes, jobs are just rows | No, Redis data structures |
| Job pickup latency | Polls the DB (default every 100ms) | Near-instant (Redis blocking pop) |
| Peak throughput | High; tuned for product workloads | Higher ceiling on dedicated Redis |
Safe under contention
Run Sidequest on as many workers and nodes as you like against the same database. The natural question when you drop the dedicated broker is: how does a job reach exactly one worker when many are polling at once?
On Postgres and MySQL, each poll claims jobs inside a single transaction with
SELECT ... FOR UPDATE SKIP LOCKED, then flips them to claimed. Concurrent
dispatchers skip the rows another worker has already locked, so a job is never handed out twice, and jobs
are taken oldest first. This is the same row-locking primitive pg-boss relies on.
-- Each poll, inside one transaction:
SELECT * FROM sidequest_jobs
WHERE state = 'waiting' AND queue = $1
AND available_at <= now()
ORDER BY inserted_at
LIMIT $2
FOR UPDATE SKIP LOCKED;
UPDATE sidequest_jobs
SET state = 'claimed', claimed_by = $3
WHERE id IN (...selected ids);
On SQLite, where SKIP LOCKED is not available, the single-writer lock
serializes writes and the claim re-checks the waiting state before taking a job, so it cannot
double-issue (run a single writer). Every backend's claim path is verified by a shared conformance suite
that asserts no double-delivery under concurrency.
When BullMQ is the better choice
Sidequest does not try to out-benchmark BullMQ on a dedicated Redis cluster. Reach for a Redis-based queue when:
- You need extreme throughput (tens of thousands of jobs per second).
- You need the lowest possible job pickup latency. Sidequest polls the database (default every 100ms); a Redis blocking pop is near-instant.
- You want to keep job traffic off your primary database, or queue infrastructure is a first-class concern separate from your application data.
- You already operate Redis and it is not an additional cost or burden.
For most product teams already running Postgres, none of these apply, and dropping Redis is a real simplification. Sidequest's polling interval and concurrency are tunable, so the latency and load are a deliberate, bounded trade for one fewer service to run.
The same job, fewer moving parts
// BullMQ: Queue + Worker + Redis
const q = new Queue("emails", { connection });
await q.add("send", { to, subject }, { attempts: 3 });
new Worker("emails", async (job) => {
await send(job.data.to, job.data.subject);
}, { connection }); // Sidequest: one Job class, your DB
class EmailJob extends Job {
async run(to, subject) {
await send(to, subject);
}
}
await Sidequest.build(EmailJob)
.maxAttempts(3)
.enqueue(to, subject); Drop Redis from your job queue
If you are already running Postgres, MySQL, SQLite, or MongoDB, Sidequest needs zero extra infrastructure.
Start in minutes
Sidequest.js