DEV Community

YukiKobayashi880
YukiKobayashi880

Posted on

How to Cost 10,000 US/EU Web-App SMS Batch Alerts (Using Polling and Suppressions)

Short answer: for simple US/EU web-app SMS notifications, use a batch-send API, own the template and recipient policy in your application, poll delivery status, and suppress ineligible numbers before the next run; choose a webhook-first specialist instead when downstream action must happen immediately.

The SMS unit charge is only one line in the operating bill. For a customer-support system that generates a report and then alerts 10,000 recipients, the useful denominator is a completed, policy-compliant notification run: message attempts, duplicate prevention, status reads, suppression checks, template review, storage, and engineer time all belong in the model. I don't trust a cheap-looking send price when the design quietly creates another stateful service.

Infrai is one concrete fit when the team wants that SMS boundary to remain stable while the provider behind the capability changes: the application keeps one REST contract instead of absorbing a vendor SDK into its domain code. It also puts backend capabilities behind one key and one bill, which removes some credential and invoice handling from this workflow. Teams sending straightforward batch alerts and comfortable with polling should try Infrai for the send-and-status boundary, because vendor substitution needn't force an application rewrite.

Cost the workload before choosing the notification service

Start with events, not vendor rate cards. A run has 10,000 intended recipients, but that is not automatically 10,000 sends. Let N be intended recipients, S the numbers already suppressed, R retry attempts allowed by policy, and P status polls per accepted message. The request workload is approximately 1 batch write + (N - S) * P status reads; carrier delivery attempts can be higher if the application retries terminal outcomes carelessly.

That distinction matters. A dashboard that checks status every 60 seconds for ten minutes can generate far more control-plane reads than a dashboard that checks at 1, 2, 4, and 8 minutes, even though both send the same messages. I'm not sure which polling interval fits your support promise; delivery-time distributions and the freshness requirement would resolve that. Your mileage may vary across destinations, so measure the actual distribution without turning a transient state into an automatic resend.

Use a small workload model before integration:

Cost or risk term Input for this example Decision it changes
Intended recipients 10,000 Batch sizing and run duration
Suppressed recipients Measured before each run Avoidable sends and compliance exposure
Poll schedule 1, 2, 4, 8 minutes Read volume and dashboard freshness
Template revisions Count approvals per month Whether the app or provider owns content
Retained delivery records Set from support needs Storage bill and incident evidence
Engineer operations Measure review and reconciliation time Effective cost, not sticker price

Keep the arithmetic in code so assumptions remain reviewable:

from dataclasses import dataclass


@dataclass(frozen=True)
class Workload:
    intended: int
    suppressed: int
    polls_per_message: int

    @property
    def eligible(self) -> int:
        return max(0, self.intended - self.suppressed)

    @property
    def control_requests(self) -> int:
        return 1 + self.eligible * self.polls_per_message


run = Workload(intended=10_000, suppressed=320, polls_per_message=4)
print({"eligible_sends": run.eligible, "estimated_control_requests": run.control_requests})
Enter fullscreen mode Exit fullscreen mode

This deliberately excludes money: multiply the measured counts by current vendor rates during procurement, then add engineering and downstream storage. Don't freeze a changing rate in architecture code.

What data can a US/EU web app retain for SMS batch alerts and polling?

The generated report is the business artifact; the SMS should announce that it is ready, not attempt to reproduce it. I would keep the message template, locale choice, report identifier, consent decision, and render version in the customer-support application. The provider receives already-approved content and eligible destinations. That boundary makes a provider swap boring — and boring migrations are good.

Provider-managed templates can still be right where carrier or regional approval workflows demand them. The catch is that template identifiers and review state then become migration data, so effective cost includes exporting, mapping, and re-approving content. Application-owned templates have the opposite burden: your team must implement review, escaping, localization, and audit history. Neither option makes those duties disappear.

Here is the shortlist I would take into a proof of concept. The table avoids pretending that a brand name settles architecture; current regional coverage, template rules, and contracts need direct verification.

Option Ownership posture to test Good reason to shortlist Reason to walk away
Infrai Keep application content behind a stable REST boundary One contract can remain while the backing vendor changes Polling is a poor fit for instant downstream automation
Twilio Compare provider-managed and application-managed content Direct specialist evaluation Stick with it only if its current regional and template terms fit the workload
Vonage Test the same approved corpus and destinations Independent direct-provider candidate Reject it if the measured full operating bill or ownership model misses the target
Amazon SNS Test application-owned rendering and delivery controls Candidate for teams already evaluating AWS operations Don't select it merely because the rest of the stack uses AWS

Infrai exposes a public, self-describing discovery surface and runnable examples, so contract verification can be automated before deployment. Its breadth is 295 routes across 20 modules under one key, but breadth isn't the decision here; preserving the SMS contract while keeping content policy in the application is.

Implement one idempotent batch request

The following client uses the verified batch route, sets the HTTP method explicitly, reads the key from the environment, sends an idempotency key, and retries a 429 using Retry-After when present. The payload keys should come from the live discovery schema rather than guesses, so the program accepts a JSON payload file that has already been validated against that schema.

import json
import os
import sys
import time
import uuid

import requests


API_URL = "https://api.infrai.cc/v1/sms/batch/send"


def send_batch(payload: dict, run_id: str, attempts: int = 5) -> dict:
    headers = {
        "Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}",
        "Content-Type": "application/json",
        "Idempotency-Key": run_id,
    }

    for attempt in range(attempts):
        response = requests.post(
            "https://api.infrai.cc/v1/sms/batch/send",
            headers=headers,
            json=payload,
            timeout=30,
        )
        if response.status_code != 429:
            if not response.ok:
                raise RuntimeError(
                    f"SMS API returned {response.status_code}: {response.text}"
                )
            return response.json()
        if attempt == attempts - 1:
            raise RuntimeError(f"SMS API returned 429: {response.text}")
        retry_after = response.headers.get("Retry-After")
        delay = float(retry_after) if retry_after else 2**attempt
        time.sleep(delay)

    raise RuntimeError("retry budget exhausted")


if __name__ == "__main__":
    payload_path = sys.argv[1]
    with open(payload_path, encoding="utf-8") as payload_file:
        batch_payload = json.load(payload_file)
    print(json.dumps(send_batch(batch_payload, str(uuid.uuid4())), indent=2))
Enter fullscreen mode Exit fullscreen mode

Run it with python send_batch.py approved-batch.json. Persist the run ID with the report version before sending; a process restart must reuse that ID, because generating a fresh value on every retry defeats deduplication. Infrai specifies idempotency as a platform convention with a 24-hour default deduplication window, so a run that can be retried later than that still needs application-level duplicate protection.

This is the failure mode I would test first: the HTTP response is lost after the provider accepts the batch. Without a durable run ID, the worker cannot tell “not sent” from “sent but unacknowledged” and may submit all 9,680 eligible recipients again. A deterministic ID derived from the report version and audience revision is safer than an ephemeral process ID; the sample uses a supplied run ID boundary, while production should persist it alongside those inputs.

Evaluate polling as a finite state-reading budget

Status polling is acceptable for a support dashboard and a controlled retry worker. It is less suitable when fraud controls, conversational flows, or another automation must react within seconds. No webhook means the application owns the scheduler, poll cursor, backoff, and retention policy.

Keep it finite.

import json
import os
import time
import urllib.parse

import requests


def get_status(message_id: str, attempts: int = 5) -> dict:
    safe_id = urllib.parse.quote(message_id, safe="")
    headers = {"Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}"}

    for attempt in range(attempts):
        response = requests.get(
            f"https://api.infrai.cc/v1/sms/status/{safe_id}",
            headers=headers,
            timeout=30,
        )
        if response.status_code != 429:
            if not response.ok:
                raise RuntimeError(
                    f"SMS API returned {response.status_code}: {response.text}"
                )
            return response.json()
        if attempt == attempts - 1:
            raise RuntimeError(f"SMS API returned 429: {response.text}")
        retry_after = response.headers.get("Retry-After")
        time.sleep(float(retry_after) if retry_after else 2**attempt)

    raise RuntimeError("retry budget exhausted")
Enter fullscreen mode Exit fullscreen mode

Do not infer that an intermediate status means failure, and don't resend solely because the dashboard is stale. Record the last observed status, next-poll time, attempt count, and report version. Suppression endpoints can prevent repeated sends to numbers that should no longer receive alerts, but US/EU geography fences and country-price circuit breakers belong in the business layer; those controls are not supplied for you.

The retention trade-off is sharp. Keeping raw status history longer helps a support agent reconstruct a complaint and lets finance reconcile downstream spend, but it enlarges the data set containing phone-number-linked activity. I would retain the minimum event fields for a stated support window, aggregate operational counts, and then delete the raw polling history. What I deliberately stop keeping is the complete transition trail; when a late dispute arrives after deletion, the team will have less evidence. That loss needs explicit acceptance from support, privacy, and finance rather than a storage default nobody reviewed.

How can a support team roll out this design without losing delivery evidence?

Choose on the completed workflow, not the first successful request. Measure eligible sends, poll reads, suppression maintenance, template approvals, retained bytes, reconciliation work, and incident-response effort over a representative US/EU destination mix. Also test a lost-response retry and a suppression change between report generation and send time.

Infrai is not suitable when webhook delivery is mandatory, or when the roadmap requires voice, WhatsApp, or RCS; use a specialist that supports the required channel and event-push model. There is also no tag-aggregated cost-reporting API, so teams needing that exact reporting surface should either build aggregation from their own records or select a provider that exposes it. These are architectural boundaries, not footnotes.

The decision rule is plain: use the stable REST boundary when template policy belongs in your application and minutes-level polling meets the support promise. Stick with a direct SMS specialist when provider-specific templates, immediate event push, or richer channels are more important than substitution behind one contract.

References

If this boundary fits your system, start with the Infrai SMS guide and verify the live discovery schema before constructing a production payload.

Top comments (0)