# Queue (/docs/infrastructure/queue)

---
title: Queue
description: Where a background job waits and who hands it to a worker. Nothing is built in, pg-boss on the Postgres you already run is what the starter writes, and moving to Redis or Cloudflare Queues is one import your job code never sees.
kind: guide
package: questpie
---

A job says what to run. The adapter decides everything else, which process picks
it up, what queued work survives that process dying, and whether cron and dedup
exist at all. It is the only piece that moves when you change backends, so
`job()`, `publish()` and your handler never name a broker.

## The default

There is not one. No queue adapter is built in, and an app with no `queue` block
is still a valid app. The service hands back an empty client.

What most readers actually run is whatever `create-questpie` wrote for them, and
that is pg-boss unless they asked otherwise.

```ts title="src/questpie/server/questpie.config.ts"
import { pgBossAdapter } from "questpie/adapters/pg-boss";
import { runtimeConfig } from "questpie/app";

import { env } from "@/lib/env";

export default runtimeConfig({
	app: { url: env.APP_URL },
	db: { url: env.DATABASE_URL },
	queue: {
		adapter: pgBossAdapter({ connectionString: env.DATABASE_URL }),
	},
});
```

`--queue bullmq` writes `bullMQAdapter({ connection: { url: env.REDIS_URL } })`
instead and adds the `bullmq` and `redis` packages. `--queue none` leaves the
block out.

<Callout type="warn" title="No block, no queue">
	Codegen still generates the typed methods from your `jobs/` files, but with no
	`queue` block the service returns `{}`. `app.queue.sendEmail` is then
	undefined and dispatching throws on the missing property. Nothing falls back.
</Callout>

## Available adapters

| Adapter           | Factory and import                                                     | Needs                            | Pick it when                                                         |
| ----------------- | ---------------------------------------------------------------------- | -------------------------------- | -------------------------------------------------------------------- |
| pg-boss           | `pgBossAdapter()` from `questpie/adapters/pg-boss`                     | the Postgres you already have    | Always, until you have a reason not to. This is the starter default. |
| BullMQ            | `bullMQAdapter()` from `questpie/adapters/bullmq`                      | Redis, plus the `bullmq` package | You already run Redis and want a dedicated high-throughput queue.    |
| Cloudflare Queues | `cloudflareQueuesAdapter()` from `questpie/adapters/cloudflare-queues` | a Queues producer binding        | You deploy to Workers. It is the only one that runs there.           |
| yours             | anything implementing `QueueAdapter`                                   | whatever your broker needs       | The three above do not cover your infrastructure.                    |

None are re-exported from `questpie/queue`. `pg-boss` and `bullmq` are optional
peer dependencies, pulled in only by their own subpath. Cloudflare needs neither.

### What each one can do

An adapter declares capability flags. The consumer flags gate methods that throw
when false, and `executionTerminalState` makes a `secretPayload` publish throw.
`scheduling` throws from `.schedule()` but skips `options.cron` quietly, and
`singleton` only advertises.

| Capability                                     | pg-boss | BullMQ | Cloudflare Queues |
| ---------------------------------------------- | ------- | ------ | ----------------- |
| `longRunningConsumer`, `queue.listen()`        | yes     | yes    | no                |
| `runOnceConsumer`, `queue.runOnce()`           | yes     | yes    | no                |
| `pushConsumer`, `queue.createPushConsumer()`   | no      | no     | yes               |
| `scheduling`, `options.cron` and `.schedule()` | yes     | yes    | no                |
| `singleton`, `singletonKey` dedup              | yes     | yes    | no                |
| `executionTerminalState`, `secretPayload`      | yes     | no     | no                |

The resolved flags are on `app.queue.capabilities`. If one codebase targets more
than one runtime, read them and branch instead of catching the throw.

## Swapping

`queue` takes an adapter instance and nothing else, so moving to Redis is one
import and one expression. The generated `app.queue.<name>` methods, your
handlers and every dispatch site are untouched.

```ts title="src/questpie/server/questpie.config.ts"
import { bullMQAdapter } from "questpie/adapters/bullmq";
import { runtimeConfig } from "questpie/app";

import { env } from "@/lib/env";

export default runtimeConfig({
	app: { url: env.APP_URL },
	db: { url: env.DATABASE_URL },
	queue: {
		adapter: bullMQAdapter({ connection: { url: env.REDIS_URL } }),
	},
});
```

Two things move under you. Anything capability-gated in the table above stops
working the moment you pick an adapter without that flag, which is what makes
the jump to Cloudflare Queues the expensive one. And dispatch inside a
transaction changes route, covered in
[Transactional dispatch](/docs/infrastructure/queue/transactional-dispatch).

### pg-boss options

`PgBossAdapterOptions` is pg-boss's own `ConstructorOptions` plus one QUESTPIE
switch. Everything else, `connectionString`, `schema`, `max` and the rest, is
spread straight into `new PgBoss(...)` and stays pg-boss's to document.

| Option                      | Default | What it does                                                                                       |
| --------------------------- | ------- | -------------------------------------------------------------------------------------------------- |
| `useApplicationTransaction` | `true`  | Insert the job through the app's current Drizzle transaction. Set `false` for a separate database. |

The adapter creates each queue on demand and caches the name, so you never
declare queues yourself. It is also the only adapter that can insert the job in
your transaction, which is why it is the default.

### BullMQ options

| Option          | Required | What it does                                                      |
| --------------- | -------- | ----------------------------------------------------------------- |
| `connection`    | yes      | ioredis connection options, handed to every `Queue` and `Worker`. |
| `queuePrefix`   | no       | Redis key prefix for this app's queues and workers.               |
| `workerOptions` | no       | Extra BullMQ `WorkerOptions`, minus `connection` and `prefix`.    |

`start()` is a no-op and connections open lazily. `listen()` builds one `Worker`
per job name and maps `teamSize` to its `concurrency`. `schedule()` adds a
repeatable under `questpie:<jobName>`, and `unschedule()` removes it by name.

### Cloudflare Queues options

Pass `queue` or `enqueue`. The constructor throws if you pass neither.

| Option    | What it does                                                                                |
| --------- | ------------------------------------------------------------------------------------------- |
| `queue`   | A producer binding, or a function returning one. The function is resolved on every publish. |
| `enqueue` | Your own producer function, in place of a binding.                                          |
| `decode`  | Turns a raw pushed body into an envelope. Defaults to requiring a `jobName` string.         |

The adapter carries a `runtime: "cloudflare"` marker, and the handler from
`questpie/adapters/cloudflare` refuses to start unless the queue adapter has
both that marker and `createPushConsumer()`. Delays cap at 24 hours, and a
`startAfter` or `retryDelay` past that throws.

<Callout type="warn" title="`retryDelay` is seconds on every adapter">
	You always pass it in seconds. BullMQ multiplies by 1000 for its millisecond
	`backoff.delay`, and Cloudflare passes it through as `delaySeconds`. Do not
	pre-multiply for either one.
</Callout>

## The interface

Every adapter, built-in or yours, implements this. It is the only seam the queue
client talks to.

```ts
interface QueueAdapter {
	capabilities?: Partial<QueueAdapterCapabilities>;

	// Required.
	start(): Promise<void>;
	stop(): Promise<void>;
	publish(jobName, payload, options?, dispatchId?): Promise<string | null>;
	schedule(jobName, cron, payload, options?): Promise<void>;
	unschedule(jobName: string): Promise<void>;
	on(event: "error", handler: (error: Error) => void): void;

	// Optional. What you implement decides what the adapter can do.
	publishInTransaction?(
		tx,
		jobName,
		payload,
		options,
		dispatchId,
	): Promise<string | null>;
	transactionalPublishing?: boolean;
	inspectExecutionState?(jobName, adapterJobId): Promise<QueueExecutionState>;
	ensureQueue?(jobName, opts?): Promise<void>;
	listen?(handlers, options?): Promise<void>;
	runOnce?(handlers, options?): Promise<QueueRunOnceResult>;
	createPushConsumer?(args): QueuePushConsumerHandler;
}
```

Those six required members are the whole obligation. Leave `capabilities` off
and the runtime infers it: the three consumer flags from whether the matching
method exists, `scheduling` from `schedule` and `unschedule` both being
functions, and `singleton` from `false`. `executionTerminalState` is the one
exception, true only when you set it true and implement `inspectExecutionState`.

`questpie/queue` carries the whole contract surface, `QueueAdapter` plus the
`Queue*` record, batch and capability types. Each adapter's option type ships
from its own entry point, as `PgBossAdapterOptions` from
`questpie/adapters/pg-boss` does.

## Where each topic lives

| Topic                                            | Page                                                                        |
| ------------------------------------------------ | --------------------------------------------------------------------------- |
| Publishing inside a transaction, crash recovery  | [Transactional dispatch](/docs/infrastructure/queue/transactional-dispatch) |
| The obligations behind each optional method      | [Writing an adapter](/docs/infrastructure/queue/writing-an-adapter)         |
| `job()`, `publish()`, `PublishOptions`, handlers | [Jobs](/docs/code/jobs)                                                     |
| Starting a worker in each consumer model         | [Running a worker](/docs/code/jobs#running-a-worker)                        |
| Encrypted payloads and safe receipts             | [Secret payloads](/docs/code/jobs/secret-payloads)                          |
| The `runtimeConfig` file and its other slots     | [Configuration](/docs/ship/configuration)                                   |
