When the Reply Beats the Reminder: Closing the Race in Follow-Up Automation
A reminder job decides at one moment and sends at a later one. If the recipient replies between those moments, the reminder goes out anyway. This article proposes a design to narrow that gap. It is a conditional recommendation, not a description of any implemented system.
The race timeline
Use this timeline as the main artifact. Write one for your own flow.
- T0: reminder job selects thread A as due (no reply seen).
- T1: job builds the message.
- T2: recipient reply arrives at your mailbox or form endpoint.
- T3: reply is recorded in your store, possibly after a delay from polling or ingestion.
- T4: job sends the reminder.
The harmful ordering is T2 before T4 while T3 comes after T4. The reply existed but your system did not know yet. No local check can fully remove this, so aim to shrink the window and soften the result.
Design constraints
- Re-read thread state immediately before sending, not only at selection.
- Move the thread to a state such as reminder_sending using a conditional update that succeeds only if the state is still awaiting_reply. If it fails, skip the send.
- Record the attempt with a stable identifier before sending.
- Treat a reply recorded at any time as setting the state to replied, which blocks later reminders.
- Add a short hold between selection and send, sized by how late your ingestion can be. Measure that lag first rather than guessing it.
Decision branches
- Reply already recorded at re-check: cancel the reminder, log the reason.
- State change fails because another worker holds it: skip, do not retry in the same run.
- Send result is unknown (timeout with no confirmation): do not automatically resend. Mark the attempt uncertain and show it to a person. A local claim alone does not prove the message was or was not delivered.
- Reply ingestion is known to be stale (last successful poll older than your chosen limit): pause reminders for that channel instead of sending.
- Reply arrives after a confirmed send: keep it, set replied, send nothing further.
Worked hypothetical
Three threads are due: A, B, C. Before sending, the job re-reads. A has a reply recorded, so it is cancelled. B passes the conditional update and the send is confirmed. C passes the update but the send times out, so C becomes uncertain and goes to review. Result: 1 reminder confirmed, 1 cancelled, 1 uncertain, and 3 threads accounted for. If B's recipient had replied one minute before the send but ingestion lagged, B would still get a reminder. That residual case is why the message wording should be harmless, for example avoiding blame for silence.
Stop conditions
Stop the whole reminder run if ingestion freshness cannot be read, if the conditional update errors repeatedly, or if the count of uncertain attempts exceeds a limit you set.
Proposed acceptance tests (to be run, not yet performed)
- Insert a reply before the re-check and expect cancellation.
- Insert a reply between selection and re-check and expect cancellation.
- Run two workers on one thread and expect at most one send attempt.
- Simulate a send timeout and expect no automatic resend and a visible uncertain record.
- Make ingestion stale and expect reminders to pause.
- Insert a reply after a confirmed send and expect no second reminder.
Layered checks reduce unwanted reminders but cannot make them impossible.