---
title: "Transactional ack and push"
description: "The acknowledgement of one stage and the push of the next, in one PostgreSQL transaction."
---

> Queen MQ documentation, for AI agents
> Complete self-contained summary of Queen MQ: https://queenmq.com/llms-brief.txt
> Fetch that first when the question is about the product rather than about this page.
> Index of all pages: https://queenmq.com/llms.txt

# Transactional ack and push

The previous tutorial handed work between queues in two steps. In that order a crash between
them duplicates work; in the other order it loses it. There is no arrangement of two calls that
avoids both.

A transaction removes the choice: the ack of the input and the push of the output commit together
or not at all. The commit also fails if the lease has expired, which is what stops a slow consumer
from acknowledging work the broker has already handed to somebody else.

This is atomic handoff, not end-to-end exactly-once delivery: a lost response plus a retry still
duplicates the pushes unless their transaction ids are deterministic. What it buys is that your
pipeline's state can never disagree with itself.

```js title="examples/tutorials/js/03-transaction-ack-push.mjs"
//
// Tutorial 3 of 5: acknowledge and push in one transaction.
//
// Tutorial 2 handed work from one queue to the next in two steps: push the
// derived message, then let the loop acknowledge the source. Between those two
// steps a crash duplicates work, and in the other order it loses work.
//
// A Queen transaction closes that window: the acknowledgement of the input and
// the push of the output are one PostgreSQL transaction. Both land or neither
// does.
//
// Run it:
//   QUEEN_URL=http://localhost:6632 node 03-transaction-ack-push.mjs

import { Queen } from 'queen-mq'

const QUEEN_URL = process.env.QUEEN_URL || 'http://localhost:6632'
const RUN = Date.now().toString(36)
const ORDERS = `tut-js-tx-orders-${RUN}`
const INVOICES = `tut-js-tx-invoices-${RUN}`
const GROUP = 'tut-js-invoicing'

const INPUT = [
  { orderId: 'A-1', customer: 'acme', total: 120.5 },
  { orderId: 'B-1', customer: 'globex', total: 88.75 },
  { orderId: 'C-1', customer: 'initech', total: 310.0 },
]

let checks = 0
const assert = (condition, description) => {
  if (!condition) throw new Error(description)
  checks++
  console.log(`  ok: ${description}`)
}

const queen = new Queen({ url: QUEEN_URL, handleSignals: false })

try {
  console.log(`broker ${QUEEN_URL}`)

  for (const order of INPUT) {
    await queen.queue(ORDERS).partition(order.customer).push({ data: order })
  }
  console.log(`pushed ${INPUT.length} orders`)

  console.log('\ninvoicing')
  const invoiced = []

  // autoAck(false) is what makes this tutorial possible: the loop must not
  // acknowledge behind your back, because the acknowledgement is part of the
  // transaction below.
  await queen
    .queue(ORDERS)
    .group(GROUP)
    .subscriptionMode('all')
    .each()
    .autoAck(false)
    .limit(INPUT.length)
    .idleMillis(5000)
    .consume(async (msg) => {
      // One commit carries both operations. The ack names the consumer group
      // explicitly: the transaction builder does not read it off the message,
      // and an ack sent without it commits the wrong cursor.
      //
      // commit() throws when the broker rejects the bundle, so there is no
      // success flag to inspect here: reaching the next line means both
      // operations are durable. The Go and Rust clients hand the result back
      // instead, and there you do have to check it.
      await queen
        .transaction()
        .queue(INVOICES)
        .partition(msg.data.customer)
        .push([{
          data: {
            invoiceId: `INV-${msg.data.orderId}`,
            orderId: msg.data.orderId,
            amount: msg.data.total,
          },
        }])
        .ack(msg, 'completed', { consumerGroup: GROUP })
        .commit()

      invoiced.push(msg.data.orderId)
      console.log(`  ${msg.data.orderId} -> INV-${msg.data.orderId}`)
    })

  assert(invoiced.length === INPUT.length, `every order was invoiced once`)

  // The commit fails if the lease has expired, which is what stops a slow
  // consumer from acking work the broker has already handed to someone else.
  // Nothing to assert here: the check above is that assertion, since a failed
  // commit would have thrown.

  console.log('\nchecking the output queue')

  // The invoices went to one partition per customer, and a pop claims a single
  // partition unless you say otherwise: partitions(10) lets this one call claim
  // up to ten of them, with batch as the total budget across all of them.
  const invoices = await queen
    .queue(INVOICES)
    .batch(10)
    .partitions(10)
    .wait(true)
    .pop()

  assert(invoices.length === INPUT.length, `${INPUT.length} invoices exist`)
  const ids = invoices.map(m => m.data.orderId).sort()
  assert(
    JSON.stringify(ids) === JSON.stringify(INPUT.map(o => o.orderId).sort()),
    'each invoice matches an order, none duplicated'
  )

  // And the input queue is committed for this group: the acks were part of the
  // same transactions that produced those invoices, so the two states cannot
  // disagree.
  const leftovers = await queen.queue(ORDERS).group(GROUP).batch(10).wait(false).pop()
  assert(leftovers.length === 0, 'the source queue is committed for this group')

  await queen.queue(ORDERS).delete()
  await queen.queue(INVOICES).delete()

  console.log(`\nPASS: ${checks} checks`)
} catch (err) {
  console.error(`\nFAIL: ${err.message}`)
  process.exitCode = 1
} finally {
  await queen.close()
}
```

## Run it

Against a broker from [the quickstart](/start/quickstart), with the JavaScript client installed:

```bash
QUEEN_URL=http://localhost:6632 node 03-transaction-ack-push.mjs
```

The program checks its own outcome and exits non-zero if a check fails. Every tutorial on this
page runs in the repository's own suite: `examples/tutorials/run.sh js`.

Next: [Replay](/use/js-client/replay).

Source: https://queenmq.com/use/js-client/transaction-ack-push/index.mdx
