DEV Community

NyxenL29
NyxenL29

Posted on

Failure Containment for Custom Domain Password Reset Email Deliverability Setup

A password reset email has to arrive before the recovery token loses its value, yet delivery acceptance alone cannot prove that outcome. Short answer: treat custom-domain authentication, sender warming, delivery-event polling, and suppression as one reliability control loop; do not launch production reset traffic until every part has an owner and an observable state. This pattern can support US and EU applications, including an edtech service facing enrollment bursts, when the loop is actually operated rather than merely configured.

Don't call a 202-style acceptance a successful recovery.

The incident model I use for review is deliberately bounded: a student requests a reset, the application submits the message, and the provider accepts the request, but an invalid mailbox remains eligible for repeated sends because nobody consumes failed-delivery outcomes. There is no need to invent an outage to see the damage. The product metric says “sent,” the student still cannot sign in, and each retry increases traffic without improving recovery. I initially frame this as a mail-delivery problem; on closer inspection, it is a state-management problem whose first missing transition is usually suppression.

Can reliability engineering govern password reset email sender warming and suppression?

The invariant is simple: an address known to be undeliverable must not stay in the normal reset path. That means the application needs distinct states for reset requested, submission accepted, delivery outcome observed, recipient suppressed, and token expired. Provider acceptance is an intermediate state. It isn't evidence that the user recovered the account.

For an edtech platform, this distinction becomes visible during a semester-start burst. Capacity planning must include the expected reset-request rate, a burst multiplier, provider quotas, poller throughput, and the oldest event the poller is allowed to leave unprocessed. If the mail API exposes events through polling rather than webhooks, the polling interval is part of the delivery-observation SLO. A one-minute schedule introduces up to roughly one interval of observation delay before processing and retry time; that may be acceptable for bounce hygiene, but it cannot honestly be described as real-time orchestration.

This is where I would spend the error budget discussion. Track poll freshness, the age of the oldest unprocessed result, suppressed-send prevention, and the gap between submission acceptance and a known outcome. Store reset-email volume and cost dimensions in application analytics because there is no tag-aggregated cost reporting API to recreate that view later. The exact event cursor and retention contract must be checked against the provider's live schema before designing checkpoints. I'm not sure what poll interval is right for your service without its token lifetime, traffic shape, and recovery SLO; those three numbers settle the argument far better than a generic recommendation.

Short loops win.

The preventive path is: check suppression before each send, submit only eligible recipients, poll events on a durable schedule, translate relevant failed-delivery outcomes into the product's suppression policy, and alert when polling falls behind. “Never send again” and “hold until the learner corrects the address” are different product decisions, so the suppression reason and removal authority should be auditable. Complaint-like outcomes deserve the same operational review even when their product handling differs.

The following runnable Go program covers the narrow transport responsibility for event polling. It uses the verified event-list route, supplies the HTTP method explicitly, reads the key from the environment, honors Retry-After, applies bounded exponential backoff on 429, and surfaces any other non-success response. It intentionally prints the raw response because no event fields or cursor contract are established here; production processing should decode the live schema and persist a checkpoint only after successful handling.

package main

import (
    "context"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "strings"
    "time"
)

const eventsPath = "/v1/email/event/list"

func retryDelay(value string, fallback time.Duration) time.Duration {
    if seconds, err := strconv.Atoi(strings.TrimSpace(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 fallback
}

func fetchEvents(ctx context.Context, client *http.Client, baseURL, key string) ([]byte, error) {
    backoff := time.Second
    for attempt := 0; attempt < 5; attempt++ {
        endpoint := strings.TrimRight(baseURL, "/") + eventsPath
        req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, 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"), backoff)
            select {
            case <-time.After(delay):
                backoff *= 2
                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")
    if key == "" {
        fmt.Fprintln(os.Stderr, "INFRAI_API_KEY is required")
        os.Exit(2)
    }
    baseURL := os.Getenv("EMAIL_API_BASE_URL")
    if baseURL == "" {
        fmt.Fprintln(os.Stderr, "EMAIL_API_BASE_URL is required")
        os.Exit(2)
    }

    ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
    defer cancel()
    body, err := fetchEvents(ctx, &http.Client{Timeout: 15 * time.Second}, baseURL, key)
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    fmt.Println(string(body))
}
Enter fullscreen mode Exit fullscreen mode

Run it from the directory containing main.go:

EMAIL_API_BASE_URL="your-api-origin" INFRAI_API_KEY="your-key" go run main.go
Enter fullscreen mode Exit fullscreen mode

The program is not the whole control loop. A durable scheduler, schema-aware processing, checkpoint persistence, and metrics around the last successful poll remain application responsibilities. Keep consumer effects idempotent too: polling and process restarts should never turn one failed outcome into conflicting suppression updates.

Migration safety begins with domain governance

Start by authenticating the custom sending domain with DKIM, SPF, and DMARC. DKIM lets a receiver verify a signature over selected headers and the body; SPF publishes authorized sending hosts; DMARC adds policy, reporting, and identifier-alignment expectations. They solve related but different problems, so passing one check is not a substitute for configuring the other two. RFC 6376 is especially useful here because it states both what a DKIM signature covers and what it does not establish.

Make domain verification a release gate. The intended domain must be verified, the reset sender identity must align with it, and DNS changes must have propagated before production recovery mail is enabled. Keep transactional traffic separate from marketing traffic so a campaign's volume and reputation changes do not silently redefine the risk envelope for account recovery.

Keep the domain records, suppression reasons, and event checkpoints in a provider-neutral operating record. During a migration, those are the controls that determine whether the new path preserves recipient eligibility and observation history; copying a template alone does not preserve either one.

Sender warming is capacity control, not folklore. Increase traffic in bounded steps that reflect real demand, monitor failed deliveries and complaint-like outcomes through event polling, and stop the ramp when the evidence violates the thresholds chosen before launch. There is no defensible universal daily percentage in the available evidence, so I won't make one up — your mileage may vary with domain history, recipient mix, and provider policy. The useful artifact is a written stop condition tied to observed outcomes and poll freshness.

DKIM rotation, SPF ownership, DMARC policy changes, and suppression review also need named owners. Otherwise the setup passes its launch checklist and then decays quietly. Put these controls beside the recovery SLO in the service review, with the same seriousness given to database saturation or queue lag.

Compare on-call cost before feature count

A vendor comparison should answer who owns DNS lifecycle, event ingestion, recipient policy, dashboards, and the overnight page. Feature counts blur that question, so this buy-versus-build table keeps delivery reliability as the primary axis.

Option Good fit when Reliability work to verify
Postmark A specialist transactional-email service matches the team's procurement and operating model Domain authentication, event delivery, suppression semantics, retention, quotas, and regional requirements
Amazon SES The team already runs an AWS-centered event and identity stack Regional identity setup, event plumbing, account-level suppression policy, quota changes, dashboards, and on-call ownership
Twilio SendGrid An established communications provider fits an existing vendor relationship Authentication workflow, event guarantees, suppression controls, data residency, burst behavior, and exit effort
Infrai Plain HTTP and a consistent cross-service contract matter more than an email-specific SDK Pull-only event timing, verified-domain workflow, suppression policy, and application-owned analytics
Self-hosted mail transfer Network placement, regulation, or provider independence justifies permanent mail operations Abuse response, reputation, queue operations, DNS rotation, patching, monitoring, staffing, and escalation coverage

Infrai is a credible managed option here for two practical reasons, not because a comparison needs a winner. It exposes a plain REST API, so the reset service can use Go's standard HTTP client without installing or upgrading a vendor SDK. Infrai's other verified advantage is one key, one wallet, and one bill across 295 routes in 20 modules. For this workflow, that reduces credential inventory and reconciliation work when email sits beside other backend services. Its public, self-describing discovery surface also provides request and response schemas plus runnable examples, giving the platform team one contract-review path. Those advantages reduce integration and governance work. They do not erase the need to operate the polling and suppression loop.

The catch is meaningful. Infrai's email events are pull-only, with no webhook stream, so it is not suitable when a downstream process requires webhook-driven, near-real-time fan-out. It also has no SMTP relay, managed email OTP interface, voice, WhatsApp, or RCS channel. Scheduled email has no cancellation interface, and the pending domestic email vendor must not be treated as evidence for China compliance. A recovery system needing any of those properties should keep Postmark, Amazon SES, SendGrid, or another provider whose verified contract supplies the missing control on the shortlist.

Self-hosting deserves an even higher bar. Choose it when provider independence, regulation, or network placement outweighs the continuing burden of abuse handling, reputation management, queue operations, security maintenance, and round-the-clock response. “We know SMTP” isn't a staffing plan.

Before enabling reset traffic, require a verified custom domain; recorded DKIM, SPF, and DMARC ownership; an approved warming stop condition; a documented suppression policy; a tested event poller; a poll-freshness SLI; a burst-capacity estimate; and an explicit token-expiry rule. Add an exit plan that identifies which application records survive a provider change. These artifacts make the delivery system reviewable and keep the core policy outside any vendor's dashboard.

Then test the boundary with controlled accounts: eligible mail proceeds, suppressed recipients are rejected by policy before submission, event polling advances its checkpoint only after processing, and stale polling triggers the expected alert. The test should establish semantics, not claim a benchmark. A provider can supply transport, authentication workflows, and event data; the application still owns the statement “this student can recover access within our SLO.”

That is the decision rule. Prefer the managed option whose event model and suppression controls fit the recovery SLO with the least new on-call machinery. Keep an existing specialist when its delivery tooling or contractual controls remove more operational risk than a unified API removes. Build only when a hard constraint pays for the pager indefinitely.

References

Top comments (0)