Replay
First-class replay verbs — by message id, by endpoint + time range, or by custom filter.
Replay is the question "can I make this message happen again, exactly as it would have happened the first time?" — answered with a single call instead of by hand-rolling SQL against an outbox table.
The three replay verbs
postel.outbound.replay(opts) takes one of three shapes, distinguished by which fields are present.
By message id
await postel.outbound.replay({
messageId: "msg_2k4n8...",
freshWebhookId: false, // keep the original id so receivers can dedup
});Re-enqueues a single historical message to every endpoint that originally received it. The receiver sees the same webhook-id — if their dedup window is still open, they correctly skip it; if not, they process it again.
Pass freshWebhookId: true if you want the receiver to treat it as new (for testing receiver behavior, for instance).
By endpoint + time range
await postel.outbound.replay({
endpointId: "ep_payments_webhook",
since: "2026-05-23T00:00:00Z",
until: "2026-05-24T00:00:00Z",
types: ["order.created", "order.refunded"],
replayThroughput: 50, // messages/sec; throttles to avoid overwhelming the receiver
freshWebhookId: false,
});The canonical operational verb: "customer X's webhook receiver was down from 02:00 to 04:00 last Tuesday; replay everything they should have received."
By predicate
await postel.outbound.replay({
filter: (msg) => msg.type === "user.signup" && msg.data.plan === "enterprise",
replayThroughput: 20,
freshWebhookId: false,
});The escape hatch for replays that can't be expressed as "endpoint + time range."
What replay returns
type ReplayResult = { enqueued: number };A count of outbox rows created. Each row goes through the normal delivery pipeline — same retries, same circuit breaker, same observability hooks.
reconcile
Distinct from replay: reconcile({ endpointId, since }) finds messages that were never confirmed delivered to the given endpoint — the ids whose latest attempt is in a non-delivered state. Useful as a recurring background job: a healthy endpoint returns an empty page; an unhealthy one surfaces the backlog, which you can feed to replay.
const page = await postel.outbound.reconcile({
endpointId: "ep_payments_webhook",
since: new Date(Date.now() - 86_400_000),
limit: 500, // default 100
});
page.items; // MessageId[] — oldest-first
page.nextCursor; // resume token, or null once the backlog is exhausted
console.log(`${page.items.length} messages never confirmed delivered`);The result is bounded — at most limit ids per call, oldest-first, with nextCursor resuming exactly where the previous page ended — so a reconcile over a long outage never materializes the entire backlog in one array. It is the same { items, nextCursor } envelope every list read uses.
Throughput control
The replayThroughput setting (messages/sec) is per-replay, not global. The worker schedules outbox rows tagged with a replay batch id; the rate limiter applies before the HTTP call. A receiver under load won't be flooded by a 100k-message replay.
Read next
send()and the outbox — what gets replayed.- Retries & backoff — how replayed attempts behave when they fail.
- Endpoints — the lifecycle of the receiver-of-record.