---
title: "Replay"
description: "Reading history again: a new group backfilled from the beginning, and an existing group rewound."
---

> 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

# Replay

Acknowledging a message does not delete it. Consumption is a cursor per consumer group, and the
messages stay until retention removes them, so history is still there to be read.

Two different operations use that. A **new group** can start at the beginning instead of the tail,
which is how you backfill a consumer that did not exist when the events happened. An **existing
group** can be moved back to a timestamp with a seek, which is how you reprocess after a bug.

The distinction matters: the subscription mode applies once, when the cursor is created, and never
again. It cannot rewind a group that already exists. A seek can, and it releases any live lease,
so an in-flight batch is abandoned rather than acknowledged.

```js title="examples/tutorials/js/04-replay.mjs"
//
// Tutorial 4 of 5: replay.
//
// Acknowledging a message does not delete it. Consumption is a cursor per
// consumer group, and the messages stay until retention removes them, so a new
// group can read the whole history and an existing group can be moved back.
//
// This is the tutorial that shows what a cursor buys you: reprocessing after a
// bug, backfilling a new consumer, and auditing what was delivered, all without
// asking the producer to send anything twice.
//
// Run it:
//   QUEEN_URL=http://localhost:6632 node 04-replay.mjs

import { Queen } from 'queen-mq'

const QUEEN_URL = process.env.QUEEN_URL || 'http://localhost:6632'
const RUN = Date.now().toString(36)
const EVENTS = `tut-js-replay-${RUN}`

const EVENTS_IN = [
  { seq: 1, type: 'created' },
  { seq: 2, type: 'updated' },
  { seq: 3, type: 'shipped' },
  { seq: 4, type: 'delivered' },
]

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

const drain = async (group, expected, mode) => {
  const seen = []
  const builder = queen
    .queue(EVENTS)
    .partition('order-1')
    .group(group)
    .each()
    .limit(expected)
    .idleMillis(4000)
  if (mode) builder.subscriptionMode(mode)
  await builder.consume(async (msg) => {
    seen.push(msg.data.seq)
  })
  return seen
}

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

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

  for (const event of EVENTS_IN) {
    await queen.queue(EVENTS).partition('order-1').push({ data: event })
  }
  console.log(`pushed ${EVENTS_IN.length} events`)

  // The live consumer. It drains the lane and commits as it goes.
  console.log('\nthe live consumer')
  const live = await drain('tut-js-live', 4, 'all')
  console.log(`  saw ${live.join(', ')}`)
  assert(JSON.stringify(live) === JSON.stringify([1, 2, 3, 4]), 'the live group read the lane in order')

  // A second group, created now, after every message was already stored and
  // acknowledged by someone else. subscriptionMode('all') is what points its
  // new cursor at the beginning: the default for a new group is the tail, so
  // without it this group would sit idle waiting for the next event.
  //
  // The mode applies when the cursor is created and never again, so it cannot
  // rewind a group that already exists. That is what seek below is for.
  console.log('\na new group, backfilled from the beginning')
  const audit = await drain('tut-js-audit', 4, 'all')
  console.log(`  saw ${audit.join(', ')}`)
  assert(JSON.stringify(audit) === JSON.stringify([1, 2, 3, 4]), 'a new group replayed the whole history')

  // Nothing was re-pushed and nothing was copied: both groups read the same
  // stored messages through their own cursors.
  console.log('\nrewinding an existing group')

  // Move the live group's cursor back an hour, which is before anything in this
  // run was pushed. The seek also releases any live lease, so an in-flight batch
  // is abandoned rather than acknowledged.
  await queen.admin.seekConsumerGroup('tut-js-live', EVENTS, {
    timestamp: new Date(Date.now() - 3600 * 1000).toISOString(),
  })

  const replayed = await drain('tut-js-live', 4)
  console.log(`  saw ${replayed.join(', ')}`)
  assert(
    JSON.stringify(replayed) === JSON.stringify([1, 2, 3, 4]),
    'the rewound group read the same events again, in the same order'
  )

  // Replay is per group. The audit group was not moved, so it stays where it
  // was and sees nothing new.
  const auditAgain = await queen
    .queue(EVENTS)
    .partition('order-1')
    .group('tut-js-audit')
    .batch(10)
    .wait(false)
    .pop()
  assert(auditAgain.length === 0, 'rewinding one group left the other where it was')

  await queen.queue(EVENTS).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 04-replay.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: [Streaming](/use/js-client/streaming).

Source: https://queenmq.com/use/js-client/replay/index.mdx
