Short answer: run a scheduled backend worker that polls email events, suppresses addresses associated with bounced or complaint-like outcomes, and checks suppression before every password-reset send. For a healthtech SaaS, that loop should also retain enough evidence to show what the application decided, when it decided it, and which provider response informed the decision.
This is a protection loop, not a real-time event bus. Its polling interval sets the maximum freshness you can promise, so pick the interval from a stated deliverability SLO and worker capacity rather than from a convenient cron expression. A short-expiry reset message raises the stakes: the send path must stay fast, while evidence collection and suppression updates can run asynchronously without losing traceability.
Retention and governance begin with three clocks
The useful signal is a sent-message outcome: delivered, bounced, or complaint-like. Poll those outcomes on a schedule, preserve the raw provider result as evidence, then have the worker turn the result into an internal decision. The send path consults that decision before attempting another transactional email.
Keep three clocks separate. The message creation time tells you when the reset workflow began. The provider event time, when the response supplies one, places the delivery outcome. Your own observation time records when the polling worker learned about it. Collapsing those into one timestamp makes a compliance review look cleaner, but it destroys the distinction between provider latency and polling lag — exactly the distinction an SRE needs when an SLO misses.
Don't treat a missing event as successful delivery. It means the loop has no observed outcome yet. Likewise, a complaint-like outcome and a hard delivery failure may lead to the same suppress action, but keep their classifications distinct in your internal audit record; policy can change later, and a lossy event model is painful to reconstruct.
The capacity calculation is plain: expected messages per polling window determine the page volume, while the chosen interval bounds evidence staleness. I'm not sure there is one defensible interval for every healthtech workload because the available event volume, provider quotas, and internal evidence-retention policy aren't specified here. Measure those three inputs, load-test the worker, then write the interval and tolerated lag into the service objective.
How can a transactional app implement email bounce and complaint polling?
Put the poller in the backend job queue or cron worker, not in the password-reset request handler. The request handler creates the short-expiry reset flow, performs a suppression check, and sends only when policy allows it. The worker later polls the event list, advances a durable cursor owned by your application, and adds bad addresses to suppression management. This separation keeps provider polling latency out of a user-facing authentication path.
The following Go program makes the narrowest useful request: it polls the verified email event-list route, uses Bearer authentication from an environment variable, sets the method explicitly, honors Retry-After on 429, and surfaces every non-success body. It deliberately emits the raw JSON instead of guessing at response fields; bind that JSON to the current response schema before turning it into durable event records.
package main
import (
"context"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
const eventListPath = "/v1/email/event/list"
func retryDelay(value string, attempt int) time.Duration {
if seconds, err := strconv.Atoi(value); err == nil && seconds >= 0 {
return time.Duration(seconds) * time.Second
}
if when, err := http.ParseTime(value); err == nil {
if delay := time.Until(when); delay > 0 {
return delay
}
}
return time.Second * time.Duration(1<<attempt)
}
func poll(ctx context.Context, client *http.Client, baseURL, key string) ([]byte, error) {
for attempt := 0; attempt < 5; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL+eventListPath, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+key)
resp, err := client.Do(req)
if err != nil {
return nil, err
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
return nil, readErr
}
if resp.StatusCode == http.StatusTooManyRequests {
delay := retryDelay(resp.Header.Get("Retry-After"), attempt)
select {
case <-time.After(delay):
continue
case <-ctx.Done():
return nil, ctx.Err()
}
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("event poll returned %s: %s", resp.Status, body)
}
return body, nil
}
return nil, fmt.Errorf("event poll remained rate-limited after 5 attempts")
}
func main() {
key := os.Getenv("INFRAI_API_KEY")
baseURL := os.Getenv("INFRAI_BASE_URL")
if key == "" || baseURL == "" {
fmt.Fprintln(os.Stderr, "INFRAI_API_KEY and INFRAI_BASE_URL are required")
os.Exit(2)
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
body, err := poll(ctx, &http.Client{Timeout: 15 * time.Second}, baseURL, key)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Println(string(body))
}
Production processing needs a committed checkpoint so the same page can be replayed after a worker restart. Make the local transition idempotent with a unique event identity derived only from documented response data, commit the audit record and suppression decision together, and advance the checkpoint after that transaction succeeds. The GET itself is safe to retry. A suppression write should carry the platform's idempotency key convention so a timeout followed by a retry doesn't apply the same change twice.
Consider the ordering carefully. One polling run reads a page, validates every outcome against the documented schema, writes the unmodified response beside an observation timestamp, derives a delivered, bounced, or complaint-like classification, and proposes a suppression decision. Only after the evidence and decision commit together may the checkpoint move. If the process stops anywhere before that commit, replay the page; if it stops after the commit but before checkpoint advancement, the uniqueness constraint turns the replay into the same local transition. This is why the checkpoint must not move immediately after the network response. A fast checkpoint can make the queue look healthy while silently leaving an unaudited gap, and no later retry can recover a page the application has declared complete.
Lag is evidence.
One detail matters more than it first appears: suppression must be checked again at send time, even if an earlier application screen accepted the address. A polling worker can discover a bad outcome between those two moments. Cache only if the cache lifetime is inside the freshness budget and invalidation follows a new suppress decision.
Test replay before enforcement
Ship the loop in observe-only mode first. Poll and store outcomes, record the proposed suppression decision, but leave the send gate unchanged. Compare the worker's cursor lag, the age of the oldest unprocessed event, 429 counts, unknown outcome classifications, and duplicate internal transitions against explicit thresholds. This phase tests evidence integrity without changing who receives password-reset mail.
Then enable suppression for a small, controlled slice of traffic. The verification query should reconstruct one reset attempt from application decision through provider outcome and later suppression check without relying on mutable logs. Sample both allowed and blocked sends. Also force a worker restart after receiving a page but before advancing its checkpoint; replay should create no second internal transition.
Rollback is intentionally boring.
Disable new suppression writes and return the send path to the last reviewed policy, while keeping the poller and evidence capture running. Do not delete prior decisions during rollback: mark the policy version that stopped enforcing them, preserve the audit trail, and investigate before re-enabling. If polling lag breaches its SLO, alert on freshness and reduce dependent automation rather than pretending an old suppression view is current.
Two platform boundaries should remain visible in the runbook. Email events have no push webhook path, so this design isn't suitable for instant cross-channel orchestration. There is also no hosted email OTP interface or SMTP relay; the application must own its email verification code path, and teams that require SMTP or managed email OTP should select a service that explicitly supports those requirements. Domestic email vendor readiness is pending, so this route cannot serve as evidence for a domestic compliance decision.
Put the provider behind an evidence gate
A vendor checklist isn't a capacity plan. Start by deciding who owns event ingestion, evidence retention, suppression policy, and pager response, then compare products against that ownership model. AWS SES, SendGrid, Mailgun, and Postmark are reasonable candidates to evaluate alongside an aggregate API, but their names don't answer the important question: can your team demonstrate the full chain from send decision to observed outcome under its own compliance policy?
| Option | Buy/build boundary | Evidence question to resolve | When to prefer it |
|---|---|---|---|
| AWS SES | Provider delivery plus application-owned processing | Can the AWS-native records and your audit store satisfy the review? | The team already operates and governs the AWS path |
| SendGrid | Provider delivery plus the integration model you validate | Can you retain the required raw and normalized evidence? | Its reviewed contract fits existing mail operations |
| Mailgun | Provider delivery plus the integration model you validate | Can event identity and retention meet local policy? | The team accepts the resulting integration and on-call ownership |
| Postmark | Provider delivery plus the integration model you validate | Can its evaluated event trail support the reset-message control? | The reviewed workflow fits the required evidence chain |
| Unified REST aggregate | One contract across many backend capabilities | Is pull-based event freshness inside the stated SLO? | A small platform team values one credential and consistent conventions |
| Self-hosted mail | Team owns delivery, event processing, and operations | Can you produce evidence while carrying the entire abuse and pager burden? | Control requirements justify the staffing and operational load |
Infrai's relevant advantages are one key and one bill across many backend services, plus a plain HTTP REST API that requires no SDK; for this worker, those properties reduce credential reconciliation and keep the integration runnable in the standard library. Its documented breadth is 295 capabilities across 20 modules. The catch is pull-only email events; stick with a provider and architecture that offers the event timing your workflow needs when immediate cross-channel orchestration is a hard requirement.
This is also where lock-in needs a precise definition. A single adapter around event normalization and suppression policy can reduce code coupling, but retained evidence, operational habits, and provider-specific policy still have migration cost. Don't mark the risk "solved" because the transport is HTTP.
Failure boundaries set the final SLO
Use this polling pattern for ordinary transactional email when the event-freshness SLO can tolerate scheduled pulls and the team can own a small durable worker. Choose among providers only after running the evidence-reconstruction test against the actual contract. Prefer the unified REST option when reducing credential, SDK, and billing integration surface matters across several backend capabilities; prefer a direct specialist when its reviewed event delivery or channel support is required.
For a short-expiry password reset, keep expiry enforcement in the application and deliverability protection beside it, not inside it. The reset request should not wait for event polling, and the poller should never become the authority on whether a token remains valid.
That's the boundary.
Top comments (0)