Coming from BullMQ
NextMQ runs real BullMQ under the hood, so the code you already know just works — same Queue, Worker, and FlowProducer. The few differences all come from one upgrade: we run, scale, and operate the queue for you.
Import diff#
- import { Queue, Worker, FlowProducer } from 'bullmq'
+ import { Queue, Worker, FlowProducer } from '@nextmq/sdk'
+ import { createNextMQHandler } from '@nextmq/sdk/next'Migration checklist#
- Swap BullMQ imports for
@nextmq/sdk. - Add the
createNextMQHandlerroute and import every worker into it. - Set
NEXTMQ_CONNECTION_STRINGandNEXT_PUBLIC_APP_URL. - Remove your Redis connection and always-on worker bootstrap.
- Replace native
QueueEventswaits withjob.waitUntilFinished()or app polling. - Move repeatable setup into startup registration.
Works exactly the same#
Import these from @nextmq/sdk and use them like you always have:
new Queue(),queue.add(),queue.addBulk()new Worker(name, processor, opts)withconcurrencyandlimiterattempts,backoff,delay,priority,lifo,jobId- Job Schedulers —
upsertJobSchedulerand friends FlowProducergraphs with parent/child failure flagsdeduplication, global concurrency and rate limits- Inspection & control —
getJob,getJobCounts,retry,promote,changeDelay,changePriority,updateProgress,log,remove
Works a little differently#
| In BullMQ | In NextMQ |
|---|---|
The Worker runs the job loop in your process | We run the loop and call your processor over a signed webhook. You declare the same processor function. |
Outcome via moveToCompleted / moveToFailed | Prefer return value / throw error. The moveTo* methods also work as terminal signals — see below. |
Await results via QueueEvents pub/sub | job.waitUntilFinished() awaits completion for you — no live connection to keep open. |
| Retention defaults to keep-forever | Sensible retention is applied automatically so storage stays lean — tune it per job with removeOnComplete / removeOnFail. |
// BullMQ: the worker runs in-process.
// NextMQ: the same processor, declared in your app and run on demand.
export const emailWorker = new Worker('emails', async (job) => {
await send(job.data)
return { ok: true } // return = completed, throw = failed (with retries)
})Scaling: one worker per queue#
In BullMQ you scale a queue by running more Workerprocesses that compete for its jobs. NextMQ runs one worker per queue on our side, so scaling is simpler: set the worker's concurrency, and your serverless platform spins up as many function instances as it needs to handle the concurrent webhook calls. There's no worker fleet to run or autoscale.
A queue is competing-consumers, exactly like BullMQ: each job runs once, it isn't broadcast to every worker. To make one event fan out to several independent handlers, give each handler its own queue and produce to all of them, or model the dependency with a flow.
moveTo* methods#
For BullMQ source compatibility, moveToCompleted, moveToFailed, andmoveToDelayed are supported inside processors as terminal outcome signals. After you call one, processor execution stops — you don't need to return, throw, pass a lock token, or throw BullMQ's special errors. return / throw stays the recommended style; these are here so existing BullMQ code moves over unchanged.
new Worker('emails', async (job) => {
if (job.data.skip) {
await job.moveToCompleted({ skipped: true })
}
// unreachable when skipped
await doWork()
return { done: true }
})| Method | Terminal outcome |
|---|---|
moveToCompleted(value) | Completes the job with value — same as return value. |
moveToFailed(error) | Fails the job — same as throw error. UnrecoverableJobError skips remaining retries. |
moveToDelayed(timestamp) | Re-runs the job at the given absolute ms timestamp. |
moveToWaitingChildren waits on children created while a parent runs. NextMQ declares children up front with FlowProducerand runs the parent only after they finish, so there's nothing to wait on — model parent/child graphs with FlowProducer and you get the same result with less wiring.
try/catch can accidentally swallow them. Re-throw withisNextMQControlFlowError:import { isNextMQControlFlowError } from '@nextmq/sdk'
try {
await job.moveToCompleted({ ok: true })
} catch (err) {
if (isNextMQControlFlowError(err)) throw err
// handle real errors here
}What NextMQ handles for you#
NextMQ owns the worker loop, locks, threads, and connections — so the low-level BullMQ APIs that exist to manage those yourself simply aren't part of the SDK. There's nothing to wire up, and a managed path covers each case:
- Lock and token plumbing —
extendLock,getNextJob,processJob— is managed for you. - Parent/child graphs use
FlowProducerinstead ofmoveToWaitingChildren. - No worker threads or sandboxed processor files to configure (
useWorkerThreads). - Completion is a quick poll with
waitUntilFinished()in place of in-processcancelJob/QueueEvents. - Payloads are plain JSON — easy to log, replay, and inspect in the dashboard.
New here instead? Start with the Quickstart.