Short answer: For an urgent gaming event, send SMS first, poll its receipt for a fixed deadline, then fall back to an email attachment through an idempotent state machine.
For a gaming operations report that must reach a person quickly, start with SMS, watch the delivery receipt for a bounded period, then send the generated report as an email attachment when the text path does not reach a terminal success state. The important design choice is an explicit state machine, not a clever retry loop. It keeps integration effort visible: one adapter for each channel, one durable record of attempts, and one policy that is testable with fake clocks.
That ordering is an operational decision. A text message is useful for the short alert, while an email can carry the CSV or PDF that explains the match, shard, or fraud signal. Polling is only a bridge between those two actions. It must stop, even when a carrier never supplies a receipt.
How should urgent event notifications poll SMS before email across US and EU?
Model an attempt as data rather than as a sleep call hidden in a worker. Store an event id, player or operator address, region, channel, provider message id, attempt number, and the last observed status. Status names should come from a small internal vocabulary: queued, sent, delivered, failed, and expired. Map carrier-specific values into that vocabulary at the adapter boundary.
US and EU are routing inputs, not reasons to fork the whole workflow. Keep sender identity, consent rules, quiet hours, and retention policy in a regional configuration table. The EU path may require a stricter lawful-basis and unsubscribe review; the US path still needs opt-out handling and carrier policy checks. RFC 8058 describes one-click unsubscribe for email, which is a useful standard when the fallback message is promotional rather than purely operational.
Here is the core policy with a deliberately boring interface. The production worker persists the returned state after every transition, so a restart cannot send the attachment twice.
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from typing import Protocol
class SmsGateway(Protocol):
def send(self, destination: str, body: str, region: str) -> str: ...
def status(self, message_id: str) -> str: ...
class MailGateway(Protocol):
def send_attachment(self, destination: str, subject: str,
body: str, filename: str, content: bytes) -> str: ...
@dataclass
class Delivery:
event_id: str
destination: str
region: str
message_id: str | None = None
state: str = "queued"
checks: int = 0
def deliver_report(item: Delivery, sms: SmsGateway, mail: MailGateway,
report: bytes, now: datetime) -> Delivery:
if item.state == "queued":
item.message_id = sms.send(
item.destination,
f"Urgent game report {item.event_id} is ready; check your inbox.",
item.region,
)
item.state = "sent"
# A short, bounded poll window prevents a stuck receipt from blocking mail.
deadline = now + timedelta(minutes=3)
while item.checks < 3 and now < deadline and item.state == "sent":
raw = sms.status(item.message_id)
item.checks += 1
if raw in {"delivered", "failed"}:
item.state = raw
break
now += timedelta(minutes=1)
if item.state in {"failed", "expired", "sent"}:
item.state = "expired" if item.state == "sent" else item.state
mail.send_attachment(
item.destination,
f"Game report {item.event_id}",
"The SMS alert did not reach a terminal delivery state.",
f"report-{item.event_id}.pdf",
report,
)
return item
The loop above is intentionally a policy sketch: a real scheduler should wake the job at next_check_at instead of holding a process. An idempotency key such as event_id:channel:attempt belongs on both adapters. If a worker crashes after the provider accepts the request but before the database commit, the key lets a retry resolve to the existing send rather than creating a duplicate.
What breaks in delivery polling and retry logic?
The first failure I guard against is treating sent as success. It only means the gateway accepted the request. A receipt can arrive late, arrive out of order, or never arrive. The second is retrying every status forever. That creates a storm precisely when an upstream system is under pressure. Use exponential backoff with a cap, add jitter, and set a total deadline per event. Three checks over three minutes is a starting policy, not a universal constant.
The third failure is attaching a report before it is complete. Generate the artifact into durable object storage, record its checksum, and pass the immutable bytes or a short-lived signed URL to the mail adapter. A partial PDF is worse than a delayed one. Also record the template version and locale; support teams need to reproduce what the player saw. In one replay test, I intentionally made the report job finish after the first notification attempt: the durable checksum let the mail worker wait for the correct artifact, while an in-memory filename would have pointed at an empty temporary file. That boundary is easy to miss when a notebook prototype writes files locally, and it is exactly why the eval harness should include a slow report, a worker restart, and a duplicate event id in the same run.
Measure it.
Observability should answer one question per event: which transition consumed the budget? Emit structured records for send accepted, receipt observed, poll expired, fallback queued, and fallback delivered. Keep the provider response body out of logs when it contains addresses or message text. Your mileage may vary on receipt latency by carrier and country, so measure p50 and p95 separately for US and EU before changing the deadline.
Integration choices and the catch
The lowest-effort implementation is usually a hosted SMS gateway plus an SMTP or API mail relay behind two tiny adapters. A self-hosted modem or direct carrier connection can reduce dependency on a gateway, but it raises operational work around routing, sender registration, and regional compliance. A queue-first design adds a broker and a worker, yet it makes retries, rate limits, and dead-letter review much easier to operate.
| Choice | Integration effort | Best fit | Main trade-off |
|---|---|---|---|
| Hosted SMS + mail relay | Low | Small game operations team | External delivery policies |
| Self-hosted transport | High | Controlled network or strict data boundary | Routing and compliance work |
| Queue-first workers | Medium | High event volume | Broker and replay operations |
The catch is that this pattern is not suitable when the alert requires an interactive conversation, guaranteed sub-second delivery, or a report containing regulated data that cannot leave your controlled environment. In those cases, stick with an in-game inbox, a managed private mail system, or a human escalation process. Do not select a channel because its dashboard looks convenient; select the one whose failure semantics your team can test.
Before copying the policy, run an eval harness with fake gateways: delayed receipts, duplicate callbacks, permanent rejects, worker restarts, and a clock that jumps across a daylight-saving boundary. Track delivery completion, duplicate sends, fallback rate, and token or message cost. I care about integration effort, but a ten-line adapter that cannot be replayed is not a small integration.
Top comments (0)