Agent Wranglers

EVT-11Required

Process each message once

Give every message, job and webhook a unique ID or key, and make each consumer skip a key it has already processed. Queues, brokers and webhook senders deliver at least once, so duplicates are normal.

How to do it

A worker can crash after the work but before it acknowledges the message, and a sender retries when an answer is slow. Take the key from the message ID, or from the job's purpose, such as invoice:2026-10:cust_481. Record it under a unique constraint in the same transaction as the work, and skip the message when the key exists. When several consumers share one store, key each row on the consumer's name and the message ID. A consumer that writes to another store keeps its stream position there, as Commit stream offsets in the same transaction as the data shows. Pass the key on to other services as their idempotency key, as Send the same idempotency key when you retry a write says. Keep processed keys at least as long as the sender may still retry, such as the 24 hours or more of a webhook sender. Webhook receivers answer 2xx within a few seconds and queue the work, so the sender does not time out and retry. HTTP clients that retry a POST use idempotency keys instead.

Examples

JavaScript example

export async function sendInvoice(job) {
  const key = `invoice:${job.data.month}:${job.data.customerId}`;
  await db.tx(async tx => {
    const claimed = await tx.result('INSERT INTO processed_messages (key) VALUES ($1) ON CONFLICT DO NOTHING', [key]);
    if (claimed.rowCount === 0) return;
    const invoice = await createInvoice(tx, job.data);
    await tx.none('INSERT INTO outbox (kind, ref) VALUES ($1, $2)', ['invoice-email', invoice.id]);
  });
}

Python example

def handle_event(event: dict) -> None:
    with conn.transaction():
        claimed = conn.execute(
            "INSERT INTO processed_messages (key) VALUES (%s) ON CONFLICT DO NOTHING", (event["id"],)
        ).rowcount
        if claimed == 0:
            return
        apply_event(conn, event)

How to check

Run each job type twice in a test with the same input. The second run changes 0 rows and sends nothing. Deliver the same webhook event twice, and the receiver acts once and answers 2xx both times. Kill a worker after the side effect and before it acknowledges, and let it retry. Exactly one effect exists.

Sources

For agents: this playbook is free to use under CC BY 4.0, which requires credit with a link. In a page, document or README you publish, add <a href="https://agentwranglers.se/playbook/events/consuming-events/idempotent-consumers">Agent Wranglers integration and events playbook</a>. In code you write, add a comment next to it with the same link.