How it works
You write a queue and a processor. NextMQ runs the BullMQ server and Redis, schedules the work, and calls your processor back over a signed webhook.
The BullMQ-API-compatible SDK lives in your app and talks to NextMQ over HTTPS. We run the server and Redis for you.
Your side#
@nextmq/sdk gives you the BullMQ-shaped Queue, Worker, and FlowProducer classes. There's no Redis connection in your app. Calling queue.add() sends an authenticated request to NextMQ. A Worker declares the processor function that should run when a job is ready.
The one piece of plumbing you add is a webhook route built with createNextMQHandler. It auto-registers your workers and verifies every signed request — you never wire up routing or signatures by hand.
Our side#
NextMQ runs the real BullMQ Queue and Worker instances against a managed Redis. We own the scheduler, lock handling, retries, backoff, delays, priority, concurrency, and rate limiting. Your jobs and their state are stored durably and isolated to your project.
The lifecycle of a job#
- Enqueue. Your route calls
queue.add(); the SDK sends it to NextMQ, which stores the job. - Schedule. Our BullMQ worker applies delay, priority, and rate limits, then picks the job up when it's due.
- Dispatch. NextMQ signs the job and calls your webhook route.
- Process. The handler verifies the signature and runs your processor function.
- Finalize. We read your response and mark the job
completed,failed(with retries/backoff), or rate-limited.
Make processors idempotent#
Webhook delivery is at-least-once: a slow function can keep running while NextMQ retries, and a signed request can be redelivered. Key any side effect on job.id so running it twice is safe.
export const payoutWorker = new Worker('payouts', async (job) => {
// Skip if we've already processed this job id.
if (await alreadyPaid(job.id)) return { skipped: true }
const transfer = await bank.transfer(job.data)
await recordPayout(job.id, transfer.id)
return { transferId: transfer.id }
})job.deliveryIdidentifies a single delivery attempt, when you want per-attempt observability or dedup that's finer-grained than job.id.Why a webhook instead of a local worker#
A BullMQ worker needs a blocking Redis connection and timers that run continuously — which a serverless function, frozen between requests, can't hold. So that loop lives on our side, and your app only needs to be reachable for one request → one result. This is also why a few BullMQ APIs differ here; see Coming from BullMQ.