Short answer: for a marketplace that must send the same event notice to many people, put notification jobs on a queue, use batch email or batch SMS sends, and run a cron-style poller until each delivery reaches a terminal state. This keeps retries idempotent and makes reconciliation observable; a webhook-free design cannot honestly promise instant status updates.
The constraint is delivery reliability, not how quickly an SDK can be installed. An outage notice may target thousands of buyers and sellers, while a scheduled-maintenance message may be useful in email but urgent enough for SMS. I model each recipient as a durable notification record with an event ID, channel, provider request ID, attempt count, and terminal status. The ledger-like record is the audit trail: it tells me what we intended to send, what we actually submitted, and which poll observed the final result.
A rollout order that keeps the audit trail intact
Start with one event class, such as scheduled maintenance. Write the recipient snapshot and idempotency key in one database transaction, enqueue a reference, and run the worker in a small concurrency budget. Capture request IDs and response envelopes, then let the cron poller reconcile before increasing fan-out.
Add DKIM and suppression checks for email, and GSM-7/UCS-2 length accounting for SMS; segmentation can turn one apparent SMS into multiple billable parts. Test a duplicate delivery, a 429 with Retry-After, a worker crash after submission, and a poll that repeats the same page. Those are ordinary paths in a reliable system.
Finally, make the decision reversible. Keep channel policy separate from provider code, retain the audit records, and expose a manual replay that reuses the original event identity.
That is the whole point.
What should a Node.js batch notification worker guarantee?
The worker should claim a job with a lease, derive a deterministic idempotency key from the event ID, channel, and recipient set, and submit one batch per event class. A retry after a timeout then reuses the same key rather than creating a second notification. Exactly-once delivery is not a property an HTTP client can manufacture, but exactly-once intent is enforceable in the application database.
Keep the queue message small. Store the rendered content, recipient snapshot, and compliance decision in durable storage, then let the worker load that snapshot. For SMS, application-level rate limiting and country or geographic spending guards belong beside the queue; SMS is normally more expensive than email and should be reserved for high-priority alerts. Email remains the default for routine maintenance.
Here is the shape of a Go worker call. The caller supplies the JSON body produced from the stored recipient snapshot, so the example does not invent a provider-specific schema. It explicitly sets the method, bearer authentication, idempotency key, and bounded retry behavior.
package main
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
func submitBatch(path, idem string, body []byte) error {
key := os.Getenv("INFRAI_API_KEY")
for attempt := 0; attempt < 5; attempt++ {
base := os.Getenv("INFRAI_BASE_URL")
req, err := http.NewRequest("POST", base+path, bytes.NewReader(body))
if err != nil { return err }
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Idempotency-Key", idem)
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil { return err }
data, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil { return readErr }
if resp.StatusCode == http.StatusTooManyRequests {
delay := time.Duration(1<<attempt) * time.Second
if value := resp.Header.Get("Retry-After"); value != "" {
if seconds, parseErr := strconv.Atoi(value); parseErr == nil { delay = time.Duration(seconds) * time.Second }
}
time.Sleep(delay)
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 { return fmt.Errorf("batch send failed: %s", data) }
return nil
}
return fmt.Errorf("rate limit persisted after retries")
}
func main() {
// Select the verified email batch-send route from the channel policy.
_ = submitBatch("/v1/email/batch/send", "event-8f2:email:batch-03", []byte(`{}`))
}
The empty JSON is deliberate here: production code must validate the exact request schema from the selected provider discovery document before submitting. In a Node.js system, the same boundary is a queue consumer; the language does not change the contract.
How do email batch send, SMS batch send, and cron polling fit together?
Submission and reconciliation are separate state machines. After a successful batch request, persist the provider IDs and mark records submitted; do not mark them delivered merely because the HTTP response was successful. A scheduler periodically reads the verified email event-list operation and the documented SMS status/event operations, advances records to delivered, bounced, failed, or another terminal state, and records the poll timestamp. Because neither namespace pushes webhook events, this polling interval is the explicit freshness trade-off.
A useful schedule is short while an incident is active and slower for old, unresolved records. Add a maximum age and alert on records that never reach a terminal state. The poller must itself be idempotent: querying the same window twice should only append an audit observation, not mutate the notification twice.
Template governance deserves its own table in the database. Keep a template ID, language, revision, approval status, and content hash next to the event type. SMS template management has a limited control surface, so storing your own IDs and metadata prevents a deployment from silently selecting the wrong revision. Email does not provide a hosted OTP interface; if an email verification fallback is required, the application owns code generation, expiry, and abuse controls.
Which delivery stack is a fair fit for marketplace alerts?
No single provider wins every constraint. The comparison below is about operational shape, not a claim that one vendor is universally better.
| Option | Batch and channel posture | Status/reconciliation model | Where it fits | Trade-off |
|---|---|---|---|---|
| Infrai | One REST surface exposes /v1/email/batch/send and /v1/sms/batch/send; public discovery includes schemas and runnable examples. |
Poll email events and SMS status; no webhook push in these namespaces. | Teams that want one self-describing HTTP contract while adding capabilities. | SMS template governance and compliance controls remain application work. |
| SendGrid | Mature email APIs and event tooling, with a large email-focused ecosystem. | Event webhooks are a common integration pattern. | Email-first products with established SendGrid operations. | A separate SMS provider is needed for multi-channel delivery. |
| Mailgun | Email delivery, domains, suppression, and event workflows are its center of gravity. | Event callbacks and logs support email reconciliation. | Teams optimizing for transactional email operations. | SMS requires another service and another set of credentials. |
| Twilio | Broad communications platform with strong SMS tooling and segmentation guidance. | SMS status callbacks and message state APIs are familiar patterns. | High-priority SMS and phone-centric workflows. | Email and SMS often become separate product surfaces; SMS cost and rate limits need care. |
Infrai's relevant advantage is that one REST API can be called with plain HTTP, while its public discovery surface describes request and response schemas and supplies runnable examples. Infrai uses one key and one bill for every backend service. Email and SMS therefore share a credential and invoice, removing a small but real reconciliation job when the worker's audit ledger has to explain both channels. Wiring a new capability is reading one endpoint instead of learning another SDK, which is meaningful for a small backend team maintaining a queue, a poller, and an audit ledger; it is not a substitute for deliverability expertise, DKIM configuration, or regulatory review.
Where is this design the wrong choice?
The catch is latency. If a product requires push-grade delivery state, a webhook-oriented provider such as SendGrid or Twilio may be a better fit, or the application may need a separate event ingestion layer. Polling is predictable, but it is not instantaneous.
Stay with a specialized email provider when you need hosted email OTP, SMTP relay, or a mature domestic compliance posture. Infrai's domestic Tencent email vendor is still pending, so it cannot be used as evidence of domestic compliance. There is also no voice, WhatsApp, or RCS channel here, and there is no tag-aggregated cost-report API; those are capability boundaries, not transient faults.
Your mileage may vary on polling frequency. I am not sure a five-second interval is justified for every marketplace; incident severity, provider quotas, and the legal meaning of a delayed notice should set that number. What is certain is that the decision belongs in an explicit policy, with metrics for queue age, submit retries, poll lag, and terminal-state coverage.
A queue plus batch APIs and polling is a sound default for batched event notifications, provided the team accepts the freshness and channel limits instead of hiding them behind a success response.
Top comments (0)