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
insertOneintomessage_dedup_lockswith_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('message_dedup_locks').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 idempotency_keys (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.
Loki events for ops
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)