Core Building Blocks / Queue & Worker

The Queue Builder

Define typed queue contracts with lifecycle config, execution profiles, and result policies.

getQueueBuilder (called on a service builder) declares what work a queue can perform, how it is typed, and how jobs behave during their lifecycle. It does not define how the work is executed — that is the worker’s job.

Builder workflow

flowchart LR
    A[Call getQueueBuilder] --> B[Define Payload Schema]
    B --> C[Set Lifecycle Config]
    C --> D[Set Execution Profile]
    D --> E[Set Result Policy]
    E --> F[Add to Service]

Step 1 — Create the builder

import { imageV1ServiceBuilder } from '../imageV1ServiceBuilder.js'

const imageQueue = imageV1ServiceBuilder.getQueueBuilder(
  'processImage',
  'Process uploaded images in the background',
)

Step 2 — Define the payload schema

import { z } from 'zod'

const imagePayloadSchema = z.object({
  imageUrl: z.string().url(),
  maxWidth: z.number().optional(),
  maxHeight: z.number().optional(),
  format: z.enum(['jpeg', 'png', 'webp']).default('jpeg'),
})

imageQueue.addPayloadSchema(imagePayloadSchema)

Step 3 — Set lifecycle configuration

imageQueue.setLifecycleConfig({
  maxAttempts: 4,                  // 1 initial + 3 retries
  visibilityTimeoutMs: 30_000,     // lock expires after 30s if worker goes silent
  retryWindowMs: 3_600_000,        // retry window: 1 hour
  poisonMessageFailureThreshold: 10,
  poisonMessageAction: 'pause-worker',
})
OptionDescription
maxAttemptsTotal attempts before the job is dead-lettered (1 initial + retries)
visibilityTimeoutMsHow long a job is locked to a worker before it becomes visible again
heartbeatIntervalMsHow often workers should heartbeat to hold the lock
retryWindowMsTotal window within which retries are allowed
retryStrategyRetry strategy object controlling backoff behavior
poisonMessageFailureThresholdFailure count before treating as poison
poisonMessageAction'none' or 'pause-worker'

Step 4 — Set execution profile

imageQueue.setExecutionProfile('longRunning', {
  maxRuntimeMs: 300_000, // 5 minutes
  strict: false, // allow slight overrun
})

Use longRunning for jobs that may take minutes or hours. Use the default profile for quick tasks.

Step 5 — Set result policy

imageQueue.setResultPolicy({
  mode: 'event',
  successEventName: 'imageProcessed',
})
// Or use the convenience helper:
// imageQueue.emitResultAsEvent('imageProcessed')
ModeBehavior
noneJob completes silently
eventEmit a custom event on success
stateStore result for later retrieval
state-and-eventBoth emit and store

Step 6 — Add to the service

import { imageV1ServiceBuilder } from './imageV1ServiceBuilder.js'
import { imageQueue } from './queues/imageQueue.js'
import { imageWorker } from './workers/imageWorker.js'

export const imageV1Service = imageV1ServiceBuilder
  .addQueueDefinition(imageQueue)
  .addQueueWorkerDefinition(imageWorker)
const imageService = await imageV1ServiceBuilder.getInstance(eventBridge, { queueBridge })
await imageService.start()

Full example

import { z } from 'zod'
import { imageV1ServiceBuilder } from '../imageV1ServiceBuilder.js'

export const imageQueue = imageV1ServiceBuilder
  .getQueueBuilder(
    'processImage',
    'Process uploaded images in the background',
  )
  .addPayloadSchema(z.object({
    imageUrl: z.string().url(),
    maxWidth: z.number().optional(),
    maxHeight: z.number().optional(),
    format: z.enum(['jpeg', 'png', 'webp']).default('jpeg'),
  }))
  .setLifecycleConfig({
    maxAttempts: 4,
    visibilityTimeoutMs: 30_000,
    retryWindowMs: 3_600_000,
    poisonMessageFailureThreshold: 5,
    poisonMessageAction: 'pause-worker',
  })
  .setExecutionProfile('longRunning', {
    maxRuntimeMs: 300_000,
    strict: false,
  })
  .setResultPolicy({
    mode: 'event',
    successEventName: 'imageProcessed',
  })

Registering with the service

Pass the queue builder directly to .addQueueDefinition() on the service:

export const imageV1Service = imageV1ServiceBuilder
  .addQueueDefinition(imageQueue)
  .addQueueWorkerDefinition(imageWorker)