convex-emails

Delivery and retries

Understand state, idempotency, and unclear provider outcomes.

State model

A record starts as queued. A worker claims it atomically and changes it to processing. A successful provider response produces accepted. An authenticated delivery event produces delivered.

StateMeaning
queuedWaiting for its due time, pacing slot, or retry
processingOne worker owns the current attempt
acceptedProvider returned a message ID
deliveredProvider reported delivery, not inbox placement
bounced, complainedProvider reported a permanent bounce or complaint
failedA definitive failure or exhausted non-ambiguous budget
uncertainAcceptance is possible but unconfirmed
cancelledQueued work stopped without an ambiguous earlier attempt
suppressedA local block stopped the message before a send
testOffline worker completed without HTTP

Delivery events cannot downgrade a complaint to a bounce or delivery. Early events are retained and applied when a provider ID becomes available. Correlation uses route, provider ID, and recipient.

Two deduplication layers

Your business key deduplicates records within a component mount. The worker sends a separate stable provider key, convex-emails:<record-id>. Every retry uses that same key and message. Local records preserve deduplication until cleanup deletes them.

The built-in adapters use a 23-hour retry window from the first claim. The shorter window leaves a margin inside the provider's documented 24-hour key lifetime. Each claim checks the deadline before sending. A delayed job cannot silently retry after this deadline.

Resend can replay a response for the same key. Plunk rejects a duplicate key. The adapters therefore use different failure policies.

Retries and recovery

Retries honor Retry-After when present. Otherwise, delay grows exponentially and stops at 60 seconds. A provider-requested longer delay is preserved. The job stops if the next attempt would exceed the time or attempt budget.

A claimed worker has a 60-second lease. A lost lease produces uncertain. The component does not assume that the network request failed. Unclaimed scheduled jobs receive at most five recovery schedules.

Any earlier ambiguous attempt remains relevant. Exhausting retries, receiving a later definitive error, cancelling retries, or blocking the recipient cannot erase that ambiguity.

Resolve an uncertain record

Inspect the provider and correlate the recipient, subject, time, and message ID. Then call components.emails.lib.resolveUncertain from trusted server code.

An accepted resolution needs a provider ID that is not already assigned to another local record on that route. A failed resolution needs a reason. The operation does not resend. Only enqueue a new business key when you have evidence that another send is appropriate.

Exactly-once email delivery is not guaranteed. The component records uncertainty rather than hiding it.

On this page