## TL;DR
Duplicate surveys mean the send ran twice without knowing about the first run: a retry after a timeout, two triggers on the same close event, or parallel workers. Guarantee at-most-once: dedupe on ticket plus survey type with an idempotency key, check the sent record before sending, and make the check-and-send atomic. One ticket, one survey, ever.

## The query

```text
csat survey agent sent duplicate surveys: at-most-once delivery
```

## Use this when

- Customers get duplicate CSAT surveys
- Survey sends double-fire on retries
- You are building survey automation
- Auditing survey send reliability

## Not for

- Writing survey questions
- Analyzing CSAT results
- Deciding survey timing
- NPS program design

## Steps

### 1. Dedupe on ticket plus survey type with an idempotency key

Key every send by ticket id and survey type, and pass that key as the idempotency key to the send call. Same key twice collapses to one send, even across retries and workers. The key is the at-most-once guarantee.

Expected output: idempotency keys on all survey sends.

### 2. Check the sent record before sending

Maintain a sent-survey registry and consult it before any send. Already sent means skip. The registry is the memory that retries and duplicate triggers lack.

Expected output: sent-registry check gating every send.

### 3. Make check-and-send atomic

A check followed later by a send has a race: two workers can both check, both send. Use an atomic claim (insert-if-absent on the dedup key) so only one worker wins. Atomicity closes the race the check alone leaves open.

Expected output: atomic claim on the dedup key.

### 4. Treat retries as replays of the same send

A timeout after the send succeeded but before the response arrived must retry with the same idempotency key, not as a new send. Same key, same send: the provider dedups it. New key, new send: the customer gets two surveys.

Expected output: retry-with-same-key discipline.

### 5. Monitor sends per customer for anomalies

Track survey sends per customer per period and alert on outliers. The metric catches the dedup failures that slip through: logic bugs, key collisions, registry outages. Monitoring is the backstop for the guarantee.

Expected output: per-customer send monitoring with alerts.

## Template: the checklist

```text
At-most-once surveys:
[ ] Idempotency key per ticket plus survey type
[ ] Sent-registry check before every send
[ ] Atomic claim on the dedup key
[ ] Retries reuse the same key
[ ] Per-customer send monitoring
```

## Variant phrasings

### duplicate csat survey same customer

Steps 1 and 2. Keys plus registry.

### survey sent twice on retry

Steps 1 and 4. Same key on retry.

### csat double send race

Step 3. Atomic claim.

## Why it happens

Survey sends get triggered by ticket-close events, and close events are noisy: updates after close, reopens, merges, plus infrastructure retries on timeouts. Each trigger path was built assuming it fires once, but the event stream fires many times. Without a shared dedup key, "fire once" is a hope; with it, it is a guarantee.

## Edge cases

- Reopened tickets: decide whether a reopen earns a new survey (usually not within a window). Encode the policy in the key or the check.
- Multiple survey types: CSAT plus follow-up need separate keys but a shared per-customer frequency cap.
- Registry outage: if the sent registry is down, fail closed (do not send) rather than fail open (send duplicates). Duplicates are worse than delays.
- Key collisions across surveys: namespace keys by survey type so CSAT and NPS never share a key.

## Provenance

Resolved from the public thread: https://vectle.com/posts/pst_wu4_mVZbMdSVRCQurwW8mw
