NextMQ

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#

  1. Swap BullMQ imports for @nextmq/sdk.
  2. Add the createNextMQHandler route and import every worker into it.
  3. Set NEXTMQ_CONNECTION_STRING and NEXT_PUBLIC_APP_URL.
  4. Remove your Redis connection and always-on worker bootstrap.
  5. Replace native QueueEvents waits with job.waitUntilFinished() or app polling.
  6. 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) with concurrency and limiter
  • attempts, backoff, delay, priority, lifo, jobId
  • Job Schedulers — upsertJobScheduler and friends
  • FlowProducer graphs with parent/child failure flags
  • deduplication, global concurrency and rate limits
  • Inspection & control — getJob, getJobCounts, retry, promote, changeDelay, changePriority, updateProgress, log, remove

Works a little differently#

In BullMQIn NextMQ
The Worker runs the job loop in your processWe run the loop and call your processor over a signed webhook. You declare the same processor function.
Outcome via moveToCompleted / moveToFailedPrefer return value / throw error. The moveTo* methods also work as terminal signals — see below.
Await results via QueueEvents pub/subjob.waitUntilFinished() awaits completion for you — no live connection to keep open.
Retention defaults to keep-foreverSensible 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 }
})
MethodTerminal 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.

Heads up
These throw an internal control-flow signal to stop the processor, so a broadtry/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 FlowProducer instead of moveToWaitingChildren.
  • No worker threads or sandboxed processor files to configure (useWorkerThreads).
  • Completion is a quick poll with waitUntilFinished() in place of in-process cancelJob / QueueEvents.
  • Payloads are plain JSON — easy to log, replay, and inspect in the dashboard.

New here instead? Start with the Quickstart.