Skip to main content

The short answer

Pass a deterministic idempotency key in the headers field of messages.send(). Commune deduplicates requests with the same key within a 24-hour window — if your agent retries the same send, only one email goes out. For application-level deduplication, track (thread_id, intent) pairs in your database before sending.

Why duplicates happen

Duplicate emails are the most common production bug in agent email systems. They happen for several reasons:
  1. Network retries. Your HTTP client retries a 503 or timeout, but the first request already succeeded. The email sends twice.
  2. Queue replays. Your task queue (SQS, BullMQ, Celery) re-delivers a message after a consumer crash. The handler runs again and sends another email.
  3. Webhook double-fires. An inbound webhook fires twice (network glitch, retry logic), and your agent generates two replies to the same message.
  4. Agent loops. Agent A replies to a customer. The reply triggers a webhook. The webhook handler does not check direction and fires Agent A again. Infinite loop.
  5. LLM non-determinism. Your agent evaluates the same thread twice (e.g., periodic polling) and decides to reply both times because the LLM does not remember it already did.
Each of these is solvable. The right defense is layered: idempotency keys at the API level, deduplication at the application level, and loop guards at the agent level.

Idempotency keys

Pass an Idempotency-Key header in your send request. Commune stores the key and returns the original response for any duplicate request within 24 hours. The key should be deterministic — derived from the inputs, not randomly generated per attempt. This way, retries of the same logical send use the same key automatically.

What makes a good idempotency key

The key must be the same across retries of the same logical operation, but different for genuinely different sends.
Do not use random UUIDs as idempotency keys. A new UUID per retry defeats the purpose — each retry looks like a new request and the email sends again.

Application-level deduplication

Idempotency keys protect against retries of the same API call. But what about your agent deciding to send the same email from two different code paths? For that, track sends in your own database.

Preventing agent loops

The most dangerous duplicate is an infinite loop: your agent replies to a customer, the reply triggers a webhook, and the webhook handler fires the agent again. This burns through your send quota in minutes. Three guards to prevent loops:

1. Check message direction

The most common fix. Only process inbound messages in your webhook handler.

2. Track replied-to message IDs

Even after filtering by direction, an inbound webhook can fire twice. Track which message IDs you have already replied to.

3. Rate-limit per thread

As a safety net, cap how many times your agent can reply to a single thread within a time window. If your agent has already sent 5 replies in the last hour to the same thread, something is probably wrong.
TypeScript

Defense in depth

The strongest protection layers all three strategies: No single layer catches everything. Use all of them.

Webhooks

Webhook payload reference and signature verification for inbound events.

How does Commune thread emails?

Thread resolution mechanics — how Commune identifies which thread an inbound email belongs to.

Rate Limits

Commune’s built-in rate limits and how they protect your sender reputation.
Last modified on March 19, 2026