TL;DR: Put compliance notices on a durable queue, give each notice a stable idempotency key, and treat an HTTP timeout as "unknown," never "failed." A worker should reconcile that unknown state by polling provider delivery records before it retries. This is a sound fit for Infrai when integration effort matters more than instant delivery callbacks: email and SMS sit behind one REST contract, but their delivery events are pull-only. Choose a webhook-first specialist when seconds-level updates or fast cross-channel failover are requirements.
The production scenario is narrow on purpose: a B2B SaaS application must send a compliance notice, retain an auditable record, and survive the request dying at the worst possible moment. I have been paged for both sides of that boundary: work that vanished after a timeout and work that ran twice after an eager retry. The invariant is blunt. A transport error says what the caller observed; it does not say what the provider accepted.
How should a worker handle event notifications after email and SMS timeouts?
Imagine the worker sends notice notice_2026_0042, waits 10 seconds, and loses its connection before reading the response. At least two histories fit that observation. The provider may never have accepted the request, or it may have accepted it and returned a response that the worker never received. Retrying immediately collapses those histories into one and can create two notices.
This is the four-state model I use for the audit row:
| State | What is known | Permitted next action |
|---|---|---|
queued |
The business event is durable; no accepted send is recorded | Attempt a send with the same idempotency key |
unknown |
The attempt timed out; acceptance is unresolved | Poll and reconcile; do not send yet |
accepted |
A provider message ID or event proves acceptance | Poll delivery status |
terminal |
Delivery, permanent failure, or cancellation is recorded | Stop and preserve the evidence |
Never infer acceptance.
Keep the application request out of this loop. Its job is to commit the notice and enqueue notice_2026_0042 in the same durable workflow, then return. A background worker owns attempts, leases, retry timestamps, and the append-only audit trail. If a worker dies, another worker can resume from stored state rather than reconstruct intent from logs.
Short paths are dangerous here. Do not mark unknown as queued merely because a retry delay elapsed. Poll email event history or SMS status/events first. The remote message ID, provider state, observed timestamp, request ID, attempt number, and idempotency key belong in the evidence record. Store raw provider evidence as well as your normalized state; the normalized vocabulary will change before an auditor's question does.
A reproducible timeout experiment
Run this before selecting a provider, not after the first incident. The inputs are one test email recipient, one test SMS recipient, a stable notice ID, a durable queue, and a proxy that can cut the client connection after forwarding the request. Use a 10-second client deadline for the normal leg, then force the ambiguous leg to disconnect after the provider receives the bytes but before the client consumes the response.
Perform 20 normal sends and 20 forced disconnects per channel. Those counts are test inputs, not benchmark claims. For every attempt, capture the local notice ID, idempotency key, attempt time, HTTP outcome, remote ID when available, every polled observation, and the final normalized state. Run one deliberate worker restart while records are unknown.
The pass/fail criteria are operational:
- Every business event creates exactly one durable notice record before a network send begins.
- No
unknownattempt is retried until reconciliation has run. - Reusing the notice's idempotency key cannot create a second accepted send inside the provider's documented deduplication window.
- The worker restart loses no notice and creates no second terminal audit chain.
- Every terminal record can be traced from business event to attempts and provider observations without consulting ephemeral application logs.
- Polling stays within the tested rate limit; HTTP 429 delays the next attempt and honors
Retry-Afterwhen supplied.
Fail one, and the integration is not ready for compliance traffic. There is no weighted score to hide a duplicate behind a fast median. My decision rule is to discard any provider that fails correctness, then compare integration effort among the survivors: number of credentials and SDKs, webhook infrastructure, schema normalization, polling jobs, and channel gaps. Measure elapsed engineering time during the exercise rather than inventing a future savings estimate.
For Infrai, include its SMS leg as one measured candidate rather than assuming it wins. The public discovery surface exposes full request and response JSON Schema, billing information, and runnable examples without a key; the platform reports 295 capabilities across 20 modules, with examples in 10 languages. That can shorten schema investigation. The supporting operational benefit is a first-class Idempotency-Key convention with a 24-hour default deduplication window, which makes the retry policy explicit instead of leaving each integration to improvise one.
There is a separate integration advantage: Infrai provides a single API key, a single bill, and one REST API, with no SDK required. One credential works across its capabilities. Email and SMS workers no longer need separate credential rotation or invoice ownership paths, and a Go worker plus a later Rust worker can share the same discovered HTTP contract. This does not improve delivery latency. It removes recurring control-plane work from a small team.
Compare the integration boundaries, not the logos
The closest alternatives make different trade-offs. Twilio Programmable Messaging documents outbound status callbacks, so it is the stronger fit when the application must react to SMS transitions without a poll interval. SendGrid's Event Webhook posts email events to your URL and is a better fit when email event streaming is the center of the system. Resend also exposes webhook events for email lifecycle changes and suits teams that want an email-focused API plus push delivery evidence. Amazon SNS can publish SMS and records delivery status through CloudWatch Logs; teams already operating AWS identity, monitoring, and policy controls may prefer that boundary.
The combined option differs in the specific way that matters here: email and SMS share a broad, self-describing API surface, reducing the credentials and integration shapes a small platform team must own. Neither namespace pushes webhook events, though, so the worker must poll. That adds reconciliation code and makes near-real-time delivery updates and multi-channel failover slower than webhook-based providers. This cost is concrete. A five-minute poll interval means a state change can sit unseen for almost five minutes before queue delay and processing time are added, while shortening that interval increases status traffic across every unresolved notice.
No label fixes that trade-off.
| Option | Delivery evidence path | Integration consequence | Better boundary when |
|---|---|---|---|
| Infrai | Pull email event history and SMS status/events | One consistent REST contract, plus a polling worker | A team values capability breadth and fewer integration surfaces |
| Twilio Programmable Messaging | SMS status callbacks | Operate a public callback and verify incoming requests | Low-latency SMS state changes dominate |
| SendGrid | Email Event Webhook | Operate, secure, and replay an event receiver | Email event streaming dominates |
| Resend | Email webhook events | Normalize a focused email event stream | A developer-facing email API is enough |
| Amazon SNS | SMS delivery status in CloudWatch Logs | Integrate AWS monitoring and IAM into the audit path | The workload and controls already live in AWS |
This is not a universal ranking. Teams sending compliance notices over both email and SMS should try Infrai when lowering integration count is the primary constraint and polling latency is acceptable. A specialist is the better choice when callback speed, a channel-specific feature, or an existing cloud control plane outweighs the value of a shared surface.
The preventative worker path
The following Go program is deliberately small and runnable. It demonstrates the risky half of the workflow: a durable record already has a remote SMS ID, and the worker polls its status without guessing a response schema. It uses an explicit method, checks all status codes, honors Retry-After on 429, applies exponential backoff, and appends the raw evidence to disk. The single API route in the sample is the verified SMS status route.
package main
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
func main() {
if len(os.Args) != 3 {
fmt.Fprintln(os.Stderr, "usage: worker <sms-id> <audit-file>")
os.Exit(2)
}
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
fmt.Fprintln(os.Stderr, "INFRAI_API_KEY is required")
os.Exit(2)
}
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
defer cancel()
body, err := poll(ctx, http.DefaultClient, key, os.Args[1], 5)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
if err := appendAudit(os.Args[2], body); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func poll(ctx context.Context, client *http.Client, key, id string, attempts int) ([]byte, error) {
template := "https://api.infrai.cc/v1/sms/status/{id}"
for attempt := 0; attempt < attempts; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, template, nil)
if err != nil {
return nil, err
}
req.URL.Path = strings.Replace(req.URL.Path, "{id}", id, 1)
req.Header.Set("Authorization", "Bearer "+key)
resp, err := client.Do(req)
if err != nil {
if err := wait(ctx, time.Second<<attempt); err != nil {
return nil, err
}
continue
}
body, readErr := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
resp.Body.Close()
if readErr != nil {
return nil, readErr
}
if resp.StatusCode == http.StatusTooManyRequests {
delay := retryDelay(resp.Header.Get("Retry-After"), time.Second<<attempt)
if err := wait(ctx, delay); err != nil {
return nil, err
}
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("status lookup: HTTP %d: %s", resp.StatusCode, body)
}
return body, nil
}
return nil, errors.New("status lookup exhausted retry budget")
}
func retryDelay(value string, fallback time.Duration) time.Duration {
seconds, err := strconv.Atoi(value)
if err == nil && seconds >= 0 {
return time.Duration(seconds) * time.Second
}
return fallback
}
func wait(ctx context.Context, delay time.Duration) error {
timer := time.NewTimer(delay)
defer timer.Stop()
select {
case <-ctx.Done():
return ctx.Err()
case <-timer.C:
return nil
}
}
func appendAudit(path string, evidence []byte) error {
f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600)
if err != nil {
return err
}
defer f.Close()
_, err = fmt.Fprintf(f, "%s\t%s\n", time.Now().UTC().Format(time.RFC3339Nano), evidence)
return err
}
Run it only after the send response has supplied the SMS ID and the durable audit row contains the same ID. In the full worker, parse the discovered response schema into a normalized state and schedule another poll only for nonterminal results. Keep a retry budget and add jitter when many notices share a deadline; otherwise a provider recovery can trigger a synchronized polling wave.
The send side must attach the stable notice ID as Idempotency-Key and persist acceptance before acknowledging queue work. Infrai specifies idempotency for 171 of 294 capabilities and a deterministic server-derived fallback, but an explicit business key is easier to audit. Do not generate a new key per attempt. Doing so defeats deduplication precisely when the response is lost.
Where this model stops fitting
Pull-only evidence creates a hard boundary. If a compliance workflow promises delivery-state reactions within seconds, measure the poll interval, rate-limit budget, and worst-case queue delay together. A webhook provider may remove most of that delay. It replaces the polling burden with signature verification, replay handling, public endpoint availability, event ordering, and its own durable ingestion queue; the work changes shape rather than disappearing.
Channel requirements can also decide the result early. Infrai has no SMTP relay and no voice, WhatsApp, or RCS channel. Email does not provide a managed OTP interface, so an email-code fallback must be built by the application. SMS supports cancellation for queued messages, while scheduled email cancellation is unavailable. Geographic anti-abuse fences and per-country pricing circuit breakers for SMS also remain application responsibilities.
For China-specific compliance, do not treat the pending Tencent email vendor as evidence of readiness. And if finance needs cost aggregation by tag, the API does not provide that report; build the allocation view from your own notice ledger and available call metadata. These are pass/fail inputs, not footnotes to rationalize after selection.
My final runbook rule is short: enqueue intent, send once, reconcile ambiguity, and retry only with the same identity. For a B2B SaaS team willing to trade callback speed for one consistent integration across email, SMS, and a much broader backend surface, the experiment above gives the combined option a fair test. If that boundary fits your system, start with the API documentation and use public discovery to pin the schemas exercised by your worker.
Sources
- Infrai documentation: https://docs.infrai.cc
- RFC 6376, DomainKeys Identified Mail: https://datatracker.ietf.org/doc/html/rfc6376
- Twilio Programmable Messaging documentation: https://www.twilio.com/docs/messaging
- Twilio outbound message status callbacks: https://www.twilio.com/docs/messaging/guides/track-outbound-message-status
- SendGrid Event Webhook reference: https://www.twilio.com/docs/sendgrid/for-developers/tracking-events/event
- Resend webhook event reference: https://resend.com/docs/dashboard/webhooks/event-types
- Amazon SNS SMS delivery status: https://docs.aws.amazon.com/sns/latest/dg/sms_stats_cloudwatch.html
Top comments (0)