NextMQ

Workers & processing

A Worker declares the processor that runs your jobs. NextMQ runs the worker loop and calls your processor over a signed webhook — you just write the function.

Declaring a worker#

jobs/ai.ts
import { Worker } from '@nextmq/sdk'

export const summarizeWorker = new Worker('ai', async (job) => {
  await job.updateProgress(20)
  const summary = await llm.summarize(job.data.docId)
  return { summary }
}, {
  concurrency: 5,
  limiter: { max: 100, duration: 1000 }, // protect your model provider's quota
})
Note
One worker per queue, per project. A fresh Workeron cold start re-registers idempotently — you don't manage worker lifecycle by hand.

Mounting the route#

createNextMQHandler returns GET and POST handlers that verify signatures, dispatch jobs to the right worker, and serve a /health path. It must run on the Node.js runtime.

app/api/nextmq/[...path]/route.ts
import { createNextMQHandler } from '@nextmq/sdk/next'
import { summarizeWorker } from '@/jobs/ai'

export const runtime = 'nodejs'

export const { GET, POST } = createNextMQHandler({
  workers: [summarizeWorker],
})

Registration#

Registration publishes one deterministic webhook URL per queue. The handler registers its workers, but if producers can enqueue before the route is first hit, register explicitly from your bootstrap or producer path:

import { ensureWorkersRegistered, getWorkerRegistrationStatuses } from '@nextmq/sdk/next'
import { summarizeWorker } from '@/jobs/ai'

await ensureWorkersRegistered([summarizeWorker])

const statuses = await getWorkerRegistrationStatuses([summarizeWorker])

Liveness is inferred from webhook delivery, not an in-process heartbeat. Re-registration is idempotent: a new deploy can re-assert or update the canonical callback URL, and the live BullMQ worker restarts only when execution parameters change. Webhook secrets are provider-owned and are never sent during worker registration.

Tip
Schedule a cron that pings the handler's /health path to re-assert registration for queues that go idle. See Deploying to Vercel.

Worker options#

Most apps only need concurrency, rate limiting, and timeout:

OptionDescription
concurrencyMax jobs this worker processes in parallel.
limiterRate limit: { max, duration } per window.
timeoutMsMax time to wait for your webhook response. Default 30 000 ms; max 300 000 ms.
Heads up
Keep timeoutMsbelow your platform's function timeout. If the platform kills or freezes an invocation after NextMQ times out, BullMQ may retry while the old invocation is still finishing.

For long-running or stalled-job tuning, lockDuration, lockRenewTime, stalledInterval, maxStalledCount, and drainDelay are also accepted — most apps never set them. Bounds are in the SDK reference.

Signalling job outcome#

NextMQ maps your processor's behavior to job state:

  • Return a value → the job is completed with that value.
  • Throw → the job failed; retries and backoff apply.
  • Throw UnrecoverableJobError → fail immediately, skipping remaining attempts.
  • Throw RateLimitError → rate-limit and re-queue without burning an attempt.
import { Worker, UnrecoverableJobError, RateLimitError } from '@nextmq/sdk'

export const aiWorker = new Worker('ai', async (job) => {
  if (!job.data.docId) throw new UnrecoverableJobError('missing docId')

  const res = await llm.summarize(job.data.docId)
  if (res.status === 429) throw new RateLimitError(res.retryAfterMs ?? 1000)

  return { summary: res.summary }
})

Non-autorun workers

Pass { autorun: false } when a worker should be mounted in the handler but not registered by default health checks. Register it explicitly with includeNonAutorun.

const backfillWorker = new Worker('backfills', processor, { autorun: false })

await ensureWorkersRegistered([backfillWorker], {
  includeNonAutorun: true,
})
Note
worker.close() is local-only. It stops using that local declaration but does not unregister the durable server-side worker. Intentional removal is an admin operation.

Pause & resume

Pause or resume processing for a queue:

await aiQueue.pause()
await aiQueue.resume()
Heads up
Webhook delivery is at-least-once, so processors must be idempotent. See Reliability & delivery guarantees for the delivery contract and the job.id idempotency pattern.