Atomic Duplicate Guard
Status: Shipped 2026-05-17 ondev, onmasteronce verified. Replaces the previous 2-min find-then-insert guard which had a TOCTOU race that allowed concurrent dupes through.
TL;DR
- Every
POST /api/messagesclaims an atomic lock on(workspaceId, recipientPhone, recipientEmail, exact body text)before sending. - If a second identical POST arrives within 15 minutes, it gets the original message back with
duplicateOnly: trueand no new send fires. - Works WITHOUT an
Idempotency-Keyheader — automatic, server-side, no caller change required. - Idempotency-Key is still preferred when your client controls retries (24-hour window, exact-request match), but this guard exists for callers that don’t set one.
Why this exists
Real-world incident, 2026-05-15 → 2026-05-17 (Frank Sondors’ Mailforge integration → Tuco workspaceorg_3C059wfibDQCpSfA0rbXVFsoJUk):
The fix closes the first two failure modes for every caller without requiring them to send
Idempotency-Key.
What gets blocked vs. allowed
Blocked: two POSTs with byte-identical(workspaceId, recipientPhone, recipientEmail, message body) within 15 minutes.
Allowed (NOT blocked):
How it works under the hood
- POST arrives, passes auth + validation.
- Tuco computes
hash = sha256(workspaceId + recipientPhone + recipientEmail + body). - Tuco attempts
insertOneinto the internal dedup-lock store with_id = hashandexpiresAt = now + 15 min. - Mongo’s built-in unique
_idindex decides the race:- Insert succeeds → caller owns the slot. Proceeds with the send. The resulting message ID is attached to the lock so future retries get the right sibling back. Lock is auto-released if the send hard-fails before the message doc lands.
- Insert fails with E11000 → another request beat us. We look up the sibling message in
messages(workspace + recipient + body, last 15 min) and return it withduplicateOnly: true. If the sibling hasn’t been written yet (the original is still in flight), we poll for up to 3 s, then returnduplicateOnly: true, message: nullrather than risk a duplicate send. - Insert fails with any other error → guard logs the error and falls through. The request proceeds without dedup protection. The guard is built to never break the send path.
- A TTL index on
expiresAt(withexpireAfterSeconds: 0) clears the lock 15 minutes after acquisition. Mongo’s TTL monitor sweeps every ~60 s, so worst-case lock lifetime is ~16 min.
Response shapes
Normal send
Duplicate caught — sibling found
message._id is the original sibling — your CRM workflow can branch on duplicateOnly === true and treat it as a no-op success.
Duplicate caught — original still in flight
Known sharp edges
- Failed sends still return the failed sibling on retry within 15 min. If your first send ended in
status: "failed"and you immediately retry with the same body, you’ll get the failed sibling back withduplicateOnly: true. To force a fresh send: change the body, wait 15 min for the lock to TTL, or manually delete the lock (db.collection('the internal dedup-lock store').deleteOne({ _id: '<hash>' })). - Polling cost. A duplicate request that arrives before the original’s message doc lands holds the connection for up to 3 s. Real-world:
createMessageinserts the message doc within ~50 ms of acquiring the lock, so most polls resolve on the first iteration. - Lock leak on process crash. Mitigated by the TTL — locks always expire 15 min after acquisition regardless of process state.
How it interacts with Idempotency-Key
The Idempotency-Key header check at /api/messages runs before this guard. If your client sends an Idempotency-Key, retries of the same request return the original response from the internal idempotency-key store (24-hour window) without ever reaching the dedup guard.
Idempotency-Key is preferred when you control the caller. The dedup guard is the safety net for everyone else.
What’s NOT changing
(workspaceId, contactIdentifiers)unique index onleads. One phone = one lead per workspace.- One conversation thread per
(workspaceId, recipientPhone). Idempotency-Keysemantics on/api/messages(still 24 h, still preferred when callers can set it).- HubSpot/GHL workflow plugin behavior — the guard fires identically for every caller because it sits on the shared
/api/messagesendpoint.
See also
api-reference/endpoint/send-message— endpoint referencefeatures/send-messages— feature overview- Source:
src/lib/dedupGuard.ts,src/app/api/messages/route.ts(tuco-app)