Job Processing Modes
Atomic Mode
Section titled “Atomic Mode”Most jobs don’t need prepare. Call complete directly and you get atomic mode automatically — one transaction for all reads and writes:
'reserve-inventory': { attemptHandler: async ({ job, complete }) => { return complete(async ({ sql }) => { const [item] = await sql`SELECT stock FROM items WHERE id = ${job.input.id}`; if (item.stock < 1) throw new Error("Out of stock"); await sql`UPDATE items SET stock = stock - 1 WHERE id = ${job.input.id}`; return { reserved: true }; }); },}This is the default path. If you’re not sure which mode to use, start here.
Staged Mode
Section titled “Staged Mode”Use staged mode when you need to do work between two transactions — typically external API calls that shouldn’t hold a database transaction open:
'charge-payment': { attemptHandler: async ({ job, prepare, complete }) => { // Phase 1: Read state (transaction) const order = await prepare({ mode: "staged" }, async ({ sql }) => { const [row] = await sql`SELECT * FROM orders WHERE id = ${job.input.id}`; return row; }); // Transaction closed, lease renewal active
// Phase 2: External API call (no transaction) const { paymentId } = await paymentAPI.charge(order.amount);
// Phase 3: Write results (new transaction) return complete(async ({ sql }) => { await sql`UPDATE orders SET payment_id = ${paymentId} WHERE id = ${order.id}`; return { paymentId }; }); },}When to Use What
Section titled “When to Use What”Do you need to call an external API or do long-runningwork between reading and writing? ├── No → Just call complete() directly (auto-setup atomic) └── Yes → Use prepare({ mode: "staged" }) Read in prepare, do external work, write in completeIn practice, explicit prepare with a fixed mode is rarely needed. prepare({ mode: "atomic" }) does the same thing as calling complete directly but with extra ceremony. The main reason to use explicit prepare is when the mode is dynamic — determined at runtime based on job input or application state.
Auto-Setup
Section titled “Auto-Setup”When you skip prepare, Queuert infers the mode from how you call complete:
| Pattern | Mode | What happens |
|---|---|---|
return complete(...) (synchronous) | Atomic | Single transaction wraps everything |
await something; return complete(...) | Staged | Lease renewal runs between async work and complete |
This means even without prepare, you can get staged behavior by doing async work before calling complete:
'send-notification': { attemptHandler: async ({ job, complete }) => { await emailService.send(job.input.to, job.input.body);
return complete(async ({ sql }) => { await sql`UPDATE notifications SET sent = true WHERE id = ${job.input.id}`; return { sentAt: new Date().toISOString() }; }); },}Anti-Patterns
Section titled “Anti-Patterns”The exception is dynamic handlers where the mode is determined at runtime — explicit prepare is the right choice there since auto-setup can’t express conditional logic.
See Also
Section titled “See Also”See examples/showcase-processing-modes for a complete working example. See also Error Handling, Timeouts, and Job Processing reference.