QUESTPIE
CodeJobs

Workers

Publishing a job only enqueues it. This is the entrypoint you write to drain the queue, in a long-running process, a serverless tick, or a pushed batch.

View markdown

QUESTPIE does not start a worker for you. You write a small entrypoint that imports the built app and calls one of three methods. Which one you can call depends on the adapter, and calling the wrong one throws.

ModelCallWorks on
Long-running processapp.queue.listen()pg-boss, BullMQ
One batch, then exitapp.queue.runOnce()pg-boss, BullMQ
Pushed batchesapp.queue.createPushConsumer()Cloudflare Queues

A long-running worker

Write a file, run it as its own process.

src/worker.ts
import { app } from "#questpie";

await app.queue.listen({ teamSize: 5, batchSize: 3 });

console.log("Worker listening for jobs...");

Start it with bun run src/worker.ts.

listen() does five things before it returns.

  • Registers every options.cron schedule, so recurring jobs exist from boot.
  • Creates each pg-boss queue with its job's declared queuePolicy.
  • Drains any dispatch intents left over from a crash.
  • Starts consuming, and re-drains intents every five seconds.
  • Installs SIGINT and SIGTERM handlers.

It resolves to a handle whose stop() removes the signal handlers and shuts the adapter down.

The two options land differently per adapter.

Optionpg-bossBullMQ
teamSizeIgnored. pg-boss 12 has no such option.Becomes the worker concurrency.
batchSizeJobs fetched per poll.Ignored.

QUESTPIE runs a fetched pg-boss batch one job at a time. To get more throughput there, run more worker processes.

Shutdown is handled by default

Set gracefulShutdown: false to opt out. shutdownSignals changes which signals are caught. shutdownTimeoutMs defaults to 10000, after which the process is force-exited.

One batch, then exit

A serverless function or a cron tick should not hold a listener open. Drain one bounded batch and return.

A scheduled serverless function
import { app } from "#questpie";

export async function handler() {
	const { processed } = await app.queue.runOnce({ batchSize: 25 });
	return { processed };
}

runOnce() relays pending dispatch intents, processes one adapter batch, then relays again. It resolves to { processed }, the count. batchSize defaults to 10.

Pass jobs: [...] to restrict it to some of your jobs. Either the registration key or the durable name works, and it normalises both.

Pushed batches

Cloudflare Queues pushes to you. A Worker's queue export is the entrypoint, and questpie/adapters/cloudflare builds it for you.

Cloudflare Worker
import { createCloudflareWorkerHandlers } from "questpie/adapters/cloudflare";

import { app } from "#questpie";

export default createCloudflareWorkerHandlers(app);

That gives you fetch, queue and scheduled at once. Take createCloudflareQueueHandler(app) on its own if you already export the other two. Both call createPushConsumer() and translate Cloudflare's batch into the shape it wants, so you never cast.

Every delivery drains pending intents first, then hands the batch to the adapter. Nothing runs when no batch arrives. So add a Cron Trigger calling await app.queue.drain(), or committed work can sit forever. See Transactional dispatch.

Check before you call

Each adapter advertises what it supports on app.queue.capabilities. Read the flag instead of catching the throw when one codebase targets more than one runtime.

if (app.queue.capabilities.longRunningConsumer) {
	await app.queue.listen();
}
FlagGates
longRunningConsumerlisten()
runOnceConsumerrunOnce()
pushConsumercreatePushConsumer()
schedulingschedule(), unschedule()
singletonAdvertises singletonKey support.
executionTerminalStatesecretPayload

scheduling is the odd one. .schedule() throws when it is false, but options.cron registration skips quietly instead. So on Cloudflare Queues a cron job is never registered and nothing complains. The scheduled handler above is how you get it back. It publishes every job whose options.cron matches the trigger's own cron string, so declare the same expression in wrangler.toml.

questpie/queue also exports startJobWorker(app.queue, options) and runJobWorkerOnce(app.queue, options). They wrap listen() and runOnce() and add nothing. startJobWorker returns void rather than the handle, so call listen() directly when you want stop().

Delivery is at least once

A crash between broker acceptance and the saved receipt can republish. Retries carry the same dispatchId, but the broker can still deliver twice. Make handlers idempotent.

On this page