An e-commerce contact form has one hard constraint: the right support queue must receive its transactional event even when one delivery channel stalls. Comparing an email and SMS notifications API therefore starts with delivery evidence, failure classification, and recovery, not the lowest unit price.
TL;DR: Put a durable notification job between form submission and every email or SMS adapter. Give the job one stable correlation ID, treat provider acceptance as an intermediate state rather than delivery, and record normalized outcomes at low cardinality. Compare SendGrid, Postmark, Mailgun, Twilio, and MessageBird with the same replayable test corpus; do not compare their marketing pages as if email and SMS had identical delivery semantics.
How should teams compare a transactional event notifications API?
The queue needs a complete request, a predictable priority, and evidence that an alert reached its intended channel. It does not need a synchronous browser request to remain open while two downstream networks make progress. The form handler should validate the request, classify it into a queue such as returns, payments, or account-access, persist an immutable event, and return only after that event is durable. A worker can then attempt the preferred channel and apply the channel policy.
Keep the state machine small: queued, accepted, delivered, temporary_failure, and permanent_failure. "Accepted" matters. An API can accept a submission before the destination network makes a final decision, so counting a successful request as a delivered notification hides the exact failure the system is meant to detect.
Accepted is not delivered.
The routing key should come from bounded business vocabulary. Do not attach the customer's email address, phone number, order ID, free-text subject, or provider message ID as metric labels. Those values belong in a protected event record or a sampled trace when operationally necessary. A counter with channel=email, queue=returns, region=eu, and outcome=temporary_failure stays finite; a counter labeled by recipient grows with the business.
Small labels compound. With 2 channels, 3 queues, 2 regions, 5 outcomes, and 2 attempts, the planned upper bound is 120 series before infrastructure labels. Add 100,000 recipient values and the useful bound disappears. Cardinality is an architectural input, not a dashboard cleanup task.
Derive recovery before comparing adapters
Persist an idempotency key with the job and make each attempt append-only. A retry must not create a second logical notification merely because a worker lost its response. The adapter should return a normalized result plus the provider receipt needed for later reconciliation; it should never leak provider-specific statuses into routing rules.
The submission contract should stay deliberately narrow and avoid carrying recipient data through telemetry. Recipient data belongs behind the internal job boundary, with access control and retention appropriate to personal data. The worker resolves the destination and renders the approved template. If email reaches a permanent failure, policy may escalate to SMS; a timeout remains ambiguous and must be reconciled before failover to avoid duplicates. The trade-off is extra queueing infrastructure and delayed final knowledge, but the alternative couples customer-facing latency to downstream networks and makes ambiguous retries harder to contain.
DMARC deserves a design review before launch. RFC 7489 describes domain-level message authentication, reporting, and conformance using SPF and DKIM results. Alignment is part of the sending-domain design, not an adapter toggle to discover during an incident. For SMS, the corresponding operational questions differ: obtain a durable status transition, preserve consent and routing context, and separate a transport receipt from evidence that a person read the message.
Make the primary standard part of the release review rather than relying on a remembered summary. This check downloads the published RFC and fails on an HTTP error:
curl --fail --silent --show-error \
--output rfc7489.txt \
https://www.rfc-editor.org/rfc/rfc7489.txt
A comparison that tests delivery reliability
A neutral test corpus can exercise email candidates such as SendGrid, Postmark, and Mailgun, plus SMS candidates such as Twilio and MessageBird. These names define test subjects, not a shortlist or endorsement. Their product boundaries are not interchangeable, so record observations by channel instead of awarding one blended score. Run the same normal delivery, invalid destination, delayed callback, duplicate callback, timeout, and replay cases, then store the observed results in a decision record. The evidence must come from the test, because this comparison cannot infer current regional handling, callback authentication, or retry behavior from a product category alone.
| Criterion | Evidence to collect | Reject when |
|---|---|---|
| Acceptance | Request result and durable receipt | A receipt cannot be correlated to the job |
| Final outcome | Authenticated status event with documented states | Acceptance is the only observable state |
| Retry safety | Result of replaying one idempotency key | A replay creates an uncontrolled duplicate |
| Regional handling | Contractual and technical data path for US and EU traffic | Required residency or transfer constraints cannot be met |
| Export | Raw event retrieval and normalized adapter output | Operational history is trapped in a dashboard |
| Channel fit | Email authentication evidence or SMS status evidence | The channel-specific failure cannot be classified |
This method avoids a false cross-channel score. Email has domain authentication and mailbox behavior; SMS has carrier routing and handset delivery states. A single "delivery rate" blends unlike populations unless the denominator, time window, destination mix, and terminal-state definition are fixed.
Price comes after the reliability gates. Compare the expected bill for the actual channel mix, retries, regional traffic, retention, and operational export, but do not turn an advertised unit rate into the decision. A cheap accepted request that cannot be reconciled is expensive during an outage.
This queued, evidence-heavy design is not suitable for a workflow that genuinely requires a synchronous final delivery result before the caller can proceed. It also carries an operational limitation: the team owns a state machine, reconciliation, and retention policy. In a low-volume internal tool where a missed alert has little consequence, that complexity may outweigh the benefit; a single channel with a manual fallback can be the more defensible boundary.
Keep evidence, not exhaust
Retention math should be explicit. Let N be jobs per day, A average attempts per job, B retained bytes per attempt, and D retention days. Raw attempt storage is approximately N × A × B × D, before replicas and indexes. At 200,000 jobs per day, 1.2 attempts, 900 retained bytes, and 30 days, the unreplicated payload is about 6.48 GB. This is an illustrative calculation, not a benchmark; measure B from the schema you actually deploy.
Keep aggregate counters for every outcome, because they are cheap and answer whether the system is failing. Keep compact job and attempt records long enough to reconcile callbacks and meet policy. Sample successful traces aggressively only after metrics preserve the totals; retain failures at a higher rate, while redacting recipient data.
Count everything. Sample detail.
Opens are a poor reliability signal. Apple's Mail Privacy Protection can prevent senders from learning whether a recipient opened an email and can obscure IP information. The delivery SLO should therefore stop at a verifiable transport state, while queue acknowledgement or ticket handling belongs to a separate business SLO.
Store enough to replay the decision, not every byte emitted along the way. A normalized failure class, attempt number, bounded route labels, timestamps, and a protected receipt usually answer more operational questions than a permanent copy of every callback body.
Roll out with bounded blast radius
Start with shadow routing: create jobs and exercise classification without notifying customers. Then enable one support queue and one region, cap concurrency, and compare persisted job counts with normalized terminal outcomes. During migration, dual observation is useful; dual sending is usually not, because it changes the customer experience and makes duplicate analysis harder.
Promote the next queue only when delayed callbacks, retries, and reconciliation have been tested. Keep the old path available for rollback until the new worker drains correctly, then remove its write path before extending retention. The final decision should name the accepted failure modes, the cardinality budget, the retention window, and the evidence required to revisit the adapter choice.
Top comments (0)