DEV Community

EmersonPrice3718
EmersonPrice3718

Posted on

Polling vs Webhooks: Node.js Startup App SMS Alert Alternative (Choose Polling)

Short answer: for a startup marketplace that emails generated reports as attachments and sends SMS alerts when those reports are ready, choose a simple polling-based SMS service when predictable integration and explicit sender control matter more than immediate event streaming; choose a webhook-oriented provider when receipt latency or multi-channel orchestration is the hard requirement.

This is a delivery-reliability decision, not a race to find the shortest send() call. The report attachment, the email, and the SMS are three different records with three different failure boundaries. Treating “request accepted” as “buyer notified” collapses those boundaries and makes an apparently simple integration impossible to audit. Polling is less fashionable, but for modest startup traffic it can be the calmer design because the application owns the retry clock, the receipt checkpoint, and the evidence used by support.

Start with the delivery record, not the provider

Suppose a seller requests a weekly marketplace report. The report worker produces marketplace-report-18472.pdf, the email path submits the attachment, and the alert path sends a short message to a registered US or EU recipient. The minimum useful record is not merely a provider message ID. It needs an internal alert ID, tenant ID, report ID, destination region, sender identity, content-encoding class, submission time, last receipt check, terminal delivery state, attempt count, and the provider reference. Campaign and tenant cost attribution belongs in that same database because the compared polling option has no tag-level cost aggregation API.

Keep those fields under your control.

Don't guess.

That choice closes an ugly accounting gap: a provider can answer what happened to one message while the marketplace still cannot answer which report, tenant, or retry produced the charge. A unique internal alert ID should survive provider retries and should be the key used to reject duplicate business actions. For the email attachment, store a content digest and the email submission reference separately; for SMS, store the send reference and advance it through submitted, checking, delivered, or terminal-failure states. The two channels may support the same business event, but they aren't one transaction.

Payload length deserves attention before provider selection. SMS encoding affects segmentation, and a small copy edit can change how many message segments are sent. Twilio's character-limit documentation is a useful primer on GSM-7 and UCS-2 boundaries. Don't let a generated report title, curly quotation mark, or long marketplace name silently turn an alert into multiple segments. Pin the alert template, test representative US and EU phone numbers, and record the rendered encoding class alongside the send attempt.

How should a startup app compare SMS sender registration and delivery receipts?

Use a two-part gate. First, confirm that the provider's sender registration and lookup workflow covers the exact sender type and destination countries you intend to use; “US and EU” is not a single compliance regime, and I'm not sure any static comparison can settle a launch-country matrix without the provider's current country documentation and a compliance review. Second, test the receipt model under your actual support objective: how quickly must an operator distinguish submitted, delivered, and terminal failure?

For polling, begin with a short interval while a message is expected to settle, then widen the interval and stop at a defined deadline. A practical schedule might be an application policy such as 15 seconds for the first two checks, 60 seconds for the next five, and then five minutes until the support deadline. Those numbers are not provider guarantees; they're an example of bounded load. Your mileage may vary. Persist next_check_at so a process restart does not reset every timer, and claim due rows with a database lease so two workers cannot poll the same receipt concurrently.

Short answer, mechanically: sender setup is a deployment prerequisite; receipt polling is a durable background job.

The failure modes should be named before launch. A send can be rejected at validation, accepted but remain non-terminal beyond the business deadline, delivered after the email attachment has already been opened, suppressed because the recipient opted out, or duplicated by an application retry that lacked a stable idempotency key. A poller can also exceed its own rate budget. Handle 429 responses with exponential backoff and honor Retry-After when it is present. None of these states should trigger a second report generation; the report is immutable input to the notification workflow, not a side effect of each delivery attempt.

The comparison that changes the architecture

There are four credible names to put on a startup shortlist: Twilio, AWS End User Messaging SMS, Vonage, and Infrai. The useful comparison is not a guessed unit-price leaderboard, because sender fees, registration rules, countries, number types, encoding, and message segments can all change the invoice. Ask each candidate the same operational questions and reject any option that cannot provide evidence for the cells your launch requires.

Option Integration decision Receipt and sender question to verify Best fit The catch
Twilio Direct specialist integration Validate current sender registration by launch country; model segment boundaries explicitly Teams that want a dedicated communications provider and are prepared to evaluate its current product surface A direct integration becomes another credential, contract, and billing boundary in a broader backend
AWS End User Messaging SMS Keep messaging near an existing AWS operating model Validate the exact origination identity and receipt workflow for every destination Teams already governing workloads and access inside AWS It is less compelling when the goal is one vendor-neutral contract across unrelated backend capabilities
Vonage Direct specialist integration Validate supported sender identity, destination coverage, and the current receipt interface Teams that prefer a communications-focused vendor and have verified their country matrix The marketplace still owns cross-provider cost attribution and report-to-alert correlation
Infrai Use one plain REST contract and poll receipts Sender registration and lookup support explicit setup; receipt events are pull-based Small teams that value a broad backend surface behind one key and one bill, without installing another SDK Not suitable when real-time webhooks, voice, WhatsApp, RCS, or advanced multi-channel journeys are requirements

Amazon SES also belongs in the review, but as the email-attachment side of this marketplace workflow rather than as an SMS substitute. Keeping it visible prevents a misleading comparison in which the SMS decision quietly dictates the email decision; SendGrid and Postmark are other real email candidates, and each should be evaluated separately if attachment delivery, SMTP relay, or email event handling becomes the dominant constraint.

The Infrai row is interesting for a narrow architectural reason: its breadth sits behind a consistent REST surface, so adding another backend capability is another endpoint under the same key rather than another SDK and credential set. For Infrai, one key and one bill cover the platform's modules, which reduces credential rotation and invoice reconciliation across the report, notification, and storage workflow. The API is genuinely self-describing: public discovery requires no key and returns the full request JSON Schema, response schema, billing data, and runnable examples for a capability. That discovery surface reports 295 routes across 20 modules, and every documented capability ships runnable examples in 10 languages. In this workflow, suppression APIs can prevent repeated sends to opted-out numbers, while sender registration and lookup make the sender lifecycle explicit. Delivery events are polling-based, however, and geography-based anti-abuse rules or country-price circuit breakers must live in the marketplace application.

The catch is real. If a fraud alert must reach an event bus immediately, or if marketing needs branching journeys across SMS and WhatsApp, stick with a provider whose verified webhook and channel set matches that design. Likewise, a team deeply standardized on AWS may reasonably accept a service-specific interface to preserve its existing identity, procurement, and operations model. Simplicity is contextual.

Make polling boring before rollout

A poller is reliable only when its database transitions are stricter than its timer. One worker claims a due receipt row, performs one status lookup, records the raw provider reference plus the normalized state, schedules the next check, and releases the lease. Terminal states never return to the queue. Unknown states remain visible and bounded rather than being silently coerced to success. A suppression check belongs before submission, and an opt-out should prevent later retries for the same number.

Poll deliberately.

The send side needs the same discipline. Use an application-generated idempotency key derived from the immutable alert ID, set an explicit HTTP method, authenticate with a key read from the environment, reject non-success responses with their 4xx reason, and retry 429 responses with bounded backoff. The verified send route for the polling option is POST /v1/sms/send; request fields should be taken from its live discovery schema instead of inferred from a description. That last constraint matters. I've seen enough client libraries drift because somebody guessed a conventional field name, although no incident claim is needed to see why schema-generated requests are safer than handwritten assumptions.

This runnable Python client deliberately accepts the send body as JSON from the environment: the public discovery document is printed first, so the caller can construct that body from the current request schema rather than from fields invented in an article. It uses only the Python standard library. INFRAI_BASE_URL, INFRAI_API_KEY, SMS_REQUEST_JSON, and ALERT_ID must be set by the deployment environment; set the base URL to the documented versioned API base.

import json
import os
import random
import time
import urllib.error
import urllib.request


BASE_URL = os.environ["INFRAI_BASE_URL"].rstrip("/")


def request_json(method, url, *, headers=None, body=None, attempts=5):
    encoded = None if body is None else json.dumps(body).encode("utf-8")
    request_headers = {"Accept": "application/json", **(headers or {})}
    if encoded is not None:
        request_headers["Content-Type"] = "application/json"

    for attempt in range(attempts):
        request = urllib.request.Request(
            url, data=encoded, headers=request_headers, method=method
        )
        try:
            with urllib.request.urlopen(request, timeout=30) as response:
                return json.loads(response.read())
        except urllib.error.HTTPError as error:
            detail = error.read().decode("utf-8", errors="replace")
            if error.code != 429 or attempt == attempts - 1:
                raise RuntimeError(f"HTTP {error.code}: {detail}") from error
            retry_after = error.headers.get("Retry-After")
            delay = float(retry_after) if retry_after else 2**attempt + random.random()
            time.sleep(delay)

    raise RuntimeError("request attempts exhausted")


discovery = request_json(
    "GET", f"{BASE_URL}/discovery/sms.send"
)
print(json.dumps(discovery["params"], indent=2))

result = request_json(
    "POST",
    f"{BASE_URL}/sms/send",
    headers={
        "Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}",
        "Idempotency-Key": os.environ["ALERT_ID"],
    },
    body=json.loads(os.environ["SMS_REQUEST_JSON"]),
)
print(json.dumps(result, indent=2))
Enter fullscreen mode Exit fullscreen mode

Run a small failure-injection matrix before increasing traffic: duplicate the queue delivery, restart the poller after it claims a row, feed it a 429 with Retry-After, hold a message in a non-terminal state past the support deadline, suppress a destination between attempts, and render one template with a UCS-2 character. The acceptance condition is not “the endpoint returned success.” It is that each test leaves one explainable alert record, no duplicate business action, a bounded next step, and enough evidence for an operator to answer what happened.

Then roll out by destination and sender identity, not by an arbitrary percentage of all traffic. Start with one registered sender and one country, inspect terminal-state distribution and segment counts, add the second region only after its registration path and escalation runbook are complete, and keep the previous provider adapter available until all in-flight receipts have reached a terminal state. This migration shape preserves the evidence chain; switching every country at once does not.

References

Top comments (0)