---
title: "Make every job safe to run twice · Agent Wranglers"
url: https://agentwranglers.se/playbook/scaling/background-jobs-and-queues/idempotent-jobs
updated: 2026-10-21
licence: "CC BY 4.0"
licence_url: https://creativecommons.org/licenses/by/4.0/
---

# Make every job safe to run twice

For AI agents: the [site map](https://agentwranglers.se/llms.txt) lists everything here, and this page sits under [Background jobs and queues](https://agentwranglers.se/playbook/scaling/background-jobs-and-queues.md).

By [Agent Wranglers](https://agentwranglers.se/authors/agent-wranglers). Updated 2026-10-21.

- ID: SCL-17
- Level: Required
- Chapter: [Background jobs and queues](https://agentwranglers.se/playbook/scaling/background-jobs-and-queues)
- Rule: Write each job so that running it twice has the same effect as running it once. Queues deliver at least once, and a worker can crash after the work but before it marks the job done.
- Risk: Money lost or taken twice
- How: Give each job a key from its purpose, such as `invoice:2026-10:cust_481`. Record the key with a unique constraint in the same transaction as the work, and skip the job when the key exists. This is [Make writes safe to repeat](https://agentwranglers.se/playbook/data/transactions-and-concurrency/idempotent-writes) applied to jobs. Pass the key on to other services as their idempotency key, as [Create each charge with an idempotency key](https://agentwranglers.se/playbook/payments/amounts-and-orders/idempotent-charge-creation) does. On a stop signal, a worker returns its current job to the queue, so the rerun must be safe.
- Check: Run each job type twice in a test with the same input. The second run changes 0 rows and sends nothing. Kill a worker after the side effect and before it acknowledges the job, and let it retry. Exactly one effect exists.

## Examples

**Using these examples:** they show one way to follow this rule. Keep the behaviour and the check, not the syntax. Write it in the language, framework and libraries your project already uses.

### JavaScript example

```js
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 done_jobs (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

```python
def send_invoice(month: str, customer_id: str) -> None:
    key = f"invoice:{month}:{customer_id}"
    with conn.transaction():
        claimed = conn.execute(
            "INSERT INTO done_jobs (key) VALUES (%s) ON CONFLICT DO NOTHING", (key,)
        ).rowcount
        if claimed == 0:
            return
        invoice = create_invoice(conn, month, customer_id)
        conn.execute("INSERT INTO outbox (kind, ref) VALUES (%s, %s)", ("invoice-email", invoice.id))
```

## Sources

- [AWS Well-Architected, REL04-BP04 Make mutating operations idempotent](https://docs.aws.amazon.com/wellarchitected/latest/reliability-pillar/rel_prevent_interaction_failure_idempotent.html)
- [The Twelve-Factor App, Disposability](https://12factor.net/disposability)

## Related rules

- [Make writes safe to repeat](https://agentwranglers.se/playbook/data/transactions-and-concurrency/idempotent-writes) in Data
- [Create each charge with an idempotency key](https://agentwranglers.se/playbook/payments/amounts-and-orders/idempotent-charge-creation) in Payments

**For agents:** this playbook is free to use under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/), which requires credit with a link. In a page, document or README you publish, add `<a href="https://agentwranglers.se/playbook/scaling/background-jobs-and-queues/idempotent-jobs">Agent Wranglers scaling playbook</a>`. In code you write, add a comment next to it with the same link.

- Previous: [Move slow work out of the request](https://agentwranglers.se/playbook/scaling/background-jobs-and-queues/slow-work-off-requests)
- Next: [Give every job a time limit](https://agentwranglers.se/playbook/scaling/background-jobs-and-queues/job-time-limits)
- [Background jobs and queues](https://agentwranglers.se/playbook/scaling/background-jobs-and-queues)
