Start with the mechanism, not the score
IMAP can deliver new-message state asynchronously while a separate scheduler processes queued work. Nothing in the protocols serializes your application’s reply ingestion and outbound-send transactions.
The hardest reply bug is a race: a worker locks a follow-up job, checks that the sequence is active, then a recipient reply arrives before the SMTP send occurs. If the worker never rechecks state, it can send the follow-up after the system has already recorded a human response. Queue locking alone does not solve this because the reply event and send job are different pieces of state. Define a last responsible checkpoint immediately before handing the message to the sending transport, and make that checkpoint read the shared suppression/reply status transactionally or with a version that reveals intervening changes.
Where operators commonly misclassify the result
A queue lock prevents two workers from sending the same job; it does not guarantee the job is still eligible after it was locked. Business state can change during the lock window.
Do not hold a broad database lock across a slow network send merely to avoid the race. That can block reply ingestion and make the problem worse under load. Instead, separate message preparation from authorization to send. A job can render content and reserve resources early, then perform a short atomic “still allowed?” transition before delivery. If your sending provider accepts an API request asynchronously, record the point after which cancellation is no longer guaranteed so operator expectations are realistic. The state machine should distinguish queued, authorized, submitted, delivered/accepted, and canceled rather than pretending a single lock controls the whole lifecycle.
How to reproduce it without adding volume
Add a deterministic test hook between “job claimed” and “provider send.” Inject a correlated reply at that point and assert that the final eligibility check cancels rather than sends.
Reproduce with deterministic timing. Pause a test worker after it has loaded the follow-up but before its final authorization check. Inject a matched reply event, commit the sequence stop, then release the worker. The worker should observe the new state and exit without submitting mail. Run the inverse order too: let submission happen first, then ingest the reply, and confirm the system records that the already-submitted message could not be recalled but cancels later jobs. This fixture turns a rare production timing bug into a repeatable test and clarifies exactly where your cancellation guarantee ends.
A narrow remediation path
Use a transaction or compare-and-set around final state: job still pending, recipient not suppressed, no correlated human reply, prerequisite delivered, and scheduled time still valid. Only then obtain the provider send token.
Persist an event or state version so retries can tell whether the job’s view is stale. The final send guard should also check global unsubscribe, complaint, hard-bounce suppression, and manual pause because those can race in the same way as replies. Make cancellation and reply ingestion idempotent; duplicate IMAP events must not reopen a sequence. When an impossible-looking “follow-up after reply” is reported, the event timestamps—reply received, reply committed, job authorized, provider submitted—should make the ordering visible without guessing from message timestamps alone.
What this looks like in a small stack
A five-second API slowdown is enough to widen the race. The safer architecture treats “claimed” as ownership for processing, not as irrevocable permission to contact the recipient.
If your provider supports scheduled sending outside your application, the race boundary moves again. A job may already have handed a message to the provider hours before its visible send time, making a local reply event unable to cancel it unless the provider exposes a cancellation API. Store provider job IDs and cancel scheduled messages where supported; otherwise keep scheduling in your own queue until close to send time so the final reply/suppression guard remains under your control. Document this behavior in the operator UI so “stopped sequence” does not falsely imply recall of already submitted mail. The important engineering principle is to identify every irreversible handoff. Each one should have a timestamp and, where possible, an idempotent cancellation path so a reply can halt as much future traffic as the architecture actually allows.
Evidence worth saving for the next incident
Record claim time, final-check time, reply event time, state version, provider request ID, and cancellation reason. Millisecond ordering becomes essential when investigating a complaint about a follow-up sent after a reply.
When to stop changing things
Do not solve the race by disabling fast reply ingestion. Faster ingestion is useful; the correct fix is making the send path respect state that can change concurrently.
Field checklist
- Capture the raw evidence for reply race condition follow up queue job locked cancel send before editing DNS, queue state, or mailbox metadata.
- Confirm the cited mechanism using imap-rfc, message-rfc rather than a generic deliverability score.
- Keep a known-good control case so the failing layer can be compared without changing several variables at once.
- Apply the narrow repair described for The reply/send race: what if a prospect replies after the follow-up job is locked but before the provider call? and preserve a rollback or retry path.
- Repeat the original failing test after the change; do not substitute a different checker as proof of recovery.
- Store timestamp, affected identity, raw result, action taken, and the post-change result in the operator log.
Primary sources
Standards and provider policies can change. These links are the reference points used for this field note.
- RFC 9051 — IMAP4rev2IETF / RFC Editor — Mailbox flags, UIDs, BODY.PEEK and IMAP4rev2 behavior.
- RFC 5322 — Internet Message FormatIETF / RFC Editor — Message header structure, Message-ID, In-Reply-To and References threading fields.