An agent that can send a message can send it twice

Three routes could send one LinkedIn message twice. We record the start of delivery before sending and forbid requeuing it, enforced in the database.
Case study // AI SDR, built by us

An agent that can send a message can send it twice

When you cannot tell whether a send happened, retrying is the dangerous option.

AI SDR sends LinkedIn messages and connection requests for our team. Three separate routes could send the same message twice, and one button would have re-sent nine messages that had already gone out.

3routes by which one message could go out twice
9 of 75failed sends already re-sent by a later row
3sends that failed with a server error after LinkedIn may have accepted them
16 of 19deliberate breakages caught by the tests

Three routes to a duplicate

  • Both confirmations lost. The browser extension delivered the message, then both attempts to report back failed. After five minutes the system treated the send as unclaimed and another install sent it again.
  • A killed background process. The extension's background worker was shut down by the browser after delivery but before it reported. Same outcome.
  • A slow delivery. Delivery outlasted the lease, so another install claimed the row.

The server had the same shape: if the process died between sending and recording, the next start put the row back in the queue.

The rule

Before contacting LinkedIn, every sender records that delivery has started and must receive an acknowledgement. From then on, no sweep may set that row back to queued. A database constraint enforces this for every writer, including someone typing SQL by hand. A row that started delivery but was never confirmed fails with a plain instruction: sending started but was never confirmed, so it was not retried; check LinkedIn before re-sending.

If a late confirmation arrives, it wins and clears the error. If the extension cannot record the start, it sends nothing.

The follow-up, from production data

A read-only look at production found 75 failed sends, and 9 of them already had a later row with the identical message. One click on "Retry all failed" would have sent all 9 again. Three more had failed with a server error on the delivery request itself, recorded as ordinary retryable failures although LinkedIn may have accepted them.

  • Retry refuses a row that was already re-sent, inside one locked transaction, so two concurrent retries cannot both pass.
  • An ambiguous outcome is unconfirmed, not failed. A server error or no answer on the delivery request now means "check LinkedIn".
  • No automatic resend on a 503. A rate-limit answer is still retried, because LinkedIn answers that before processing.

What we gave up, on purpose

Some sends now need a person to look at LinkedIn instead of being retried. That is the price of never sending a stranger the same message twice, and it is the right price.

How we tested it

Each fix was loaded back to the code from before it and its new tests run, so we watched them fail first. A matrix of 19 deliberate breakages was run against the guards: 16 were caught, two of the survivors could not be witnessed through the code paths but are enforced by the database constraint anyway, and one real survivor was closed with a stronger assertion.

The browser extension and the server share one table of statuses, with a parity test that runs the extension's real code against the server's rule.

Does your automation know when it is not sure?

We test retries, timeouts and half-finished sends before they reach a customer. The first month is a trial.

Talk to our team
Share the Post: