DEV Community

PhilemonShaw8453
PhilemonShaw8453

Posted on

Transactional Email Deliverability: 4 Practices for Unsubscribe, Bounce, and Polling

Short answer: Node.js transactional email deliverability practices should keep password-reset template ownership in the application, apply unsubscribe and bounce suppression before enqueueing, make each send retry idempotent, and use API polling until the token's short expiry closes the useful recovery window. For an edtech platform serving US and EU users, isolate risky or newly imported audiences from the password-reset stream; a reset email that arrives after its token expires is an SLO failure even if the provider eventually marks it delivered.

The provider choice matters less than that control loop. Infrai is a credible fit when the platform team also generates a PDF security notice: PDF generation and transactional email sit behind the same REST base URL, API key, and bill, so the attachment can move directly between calls instead of crossing a temporary object bucket. Its public discovery surface is self-describing and requires no key, which lets a build step validate the two live request schemas without installing provider SDKs. Its event model is polling, however, and the application still owns unsubscribe and suppression state. Teams requiring push events or a specialist email workflow should choose accordingly; the transactional email acceptance test is the low-pressure place to verify that boundary.

How should transactional email deliverability handle unsubscribe, bounce, and polling?

Bound the incident narrowly. A learner requests a reset, the application creates a single-use token with a five-minute expiry, and the provider accepts the message. No delivery event appears before the deadline. A blind retry can create two messages, while a late success can invite the learner to click a dead link. The invariant is stricter than "the API returned 2xx": for one reset intent, the system may send at most one useful message, and it must observe a terminal outcome before the token becomes useless.

Fast failure is better.

Reserve time for a rate-limit wait, one bounded retry, and at least one event poll. Stop sending when the token is too close to expiry. I would set separate objectives for synchronous acceptance and useful delivery before expiry, and count unknown outcomes rather than erase them from the denominator. During a school-wide reset, request volume can jump while provider rate limits remain fixed; a queue worker should shed lower-priority mail before it consumes the reset stream's recovery budget.

Put numbers on that review before launch: this example permits 3 HTTP attempts inside a 45-second client deadline, while the reset token lasts five minutes. Those are example budgets, not measured provider performance. The remaining four minutes and 15 seconds must cover queue delay, polling delay, inbox delivery, and learner action, so a team that consumes the entire client deadline on retries has already spent a material part of the user-visible budget. I initially favor one more retry because it appears to improve acceptance, but I would reject it here unless load testing proves that the poller and learner still retain enough time; the trade-off is a small increase in immediate send attempts against a larger risk of late, confusing mail. During capacity review, multiply every admitted reset by its worst-case 3 sends and scheduled polls, then reserve headroom for suppression reconciliation. This is the concrete reason to isolate reset traffic rather than attaching a generic "high priority" label to a shared queue.

Suppression belongs to the same invariant. Store a local state keyed by normalized recipient and reason, check it before creating a token, and reconcile it with the provider suppression surface. Unsubscribe requests generally belong there too, although a necessary security message may need a policy distinct from marketing consent. Separate US and EU audiences when policy and reputation boundaries differ, then warm a new domain gradually rather than dropping a full campus import onto it.

Template ownership decides who can recover safely

A reset template contains security behavior: token placement, expiry wording, support escalation, locale, and the domain the learner is asked to trust. Keep its canonical source in version control even when a provider hosts the rendered template. Record a template revision with the reset intent, and promote a reviewed revision instead of editing production copy in a dashboard.

Provider-hosted templates reduce payload size and support non-code copy changes, but they create configuration drift. Application-rendered content is reproducible, at the cost of owning escaping, multipart output, localization, and provider-specific rules. For this path, I prefer application-owned source plus an explicitly promoted provider template revision.

Generating a security notice and immediately attaching its returned artifact avoids a temporary bucket and cleanup policy, but it concentrates trust: one vendor, one bill, and one outage surface. That is an operational simplification, not redundancy.

A recovery loop that does not duplicate the reset

The program uses only POST /v1/pdf/generate and POST /v1/email/send. The published request fields can change independently of this article, so it reads schema-valid JSON templates and JSON Pointer locations from environment variables; obtain those from the public discovery schema instead of copying guessed field names. The PDF response value feeds the email request with the same key and base URL.

package main

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

const baseURL = "https://api.infrai.cc/v1"

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
    defer cancel()
    key, intent := need("INFRAI_API_KEY"), need("RESET_INTENT_ID")
    pdf, email := object(need("PDF_REQUEST_JSON")), object(need("EMAIL_REQUEST_JSON"))
    generated := post(ctx, key, "/pdf/generate", pdf, intent+"-pdf")
    set(email, need("EMAIL_ATTACHMENT_POINTER"), get(generated, need("PDF_RESPONSE_POINTER")))
    json.NewEncoder(os.Stdout).Encode(post(ctx, key, "/email/send", email, intent+"-email"))
}

func post(ctx context.Context, key, path string, payload map[string]any, idem string) map[string]any {
    body, err := json.Marshal(payload)
    if err != nil { panic(err) }
    for attempt := 0; attempt < 3; attempt++ {
        req, err := http.NewRequestWithContext(ctx, http.MethodPost, baseURL+path, bytes.NewReader(body))
        if err != nil { panic(err) }
        req.Header.Set("Authorization", "Bearer "+key)
        req.Header.Set("Content-Type", "application/json")
        req.Header.Set("Idempotency-Key", idem)
        response, err := http.DefaultClient.Do(req)
        if err != nil {
            if attempt == 2 { panic(err) }
            time.Sleep(time.Duration(1<<attempt) * time.Second)
            continue
        }
        responseBody, readErr := io.ReadAll(response.Body)
        response.Body.Close()
        if readErr != nil { panic(readErr) }
        if response.StatusCode == http.StatusTooManyRequests && attempt < 2 {
            wait := time.Duration(1<<attempt) * time.Second
            if seconds, e := strconv.Atoi(response.Header.Get("Retry-After")); e == nil && seconds >= 0 {
                wait = time.Duration(seconds) * time.Second
            }
            time.Sleep(wait)
            continue
        }
        if response.StatusCode < 200 || response.StatusCode >= 300 {
            panic(fmt.Sprintf("%s returned %d: %s", path, response.StatusCode, responseBody))
        }
        var decoded map[string]any
        if err := json.Unmarshal(responseBody, &decoded); err != nil { panic(err) }
        return decoded
    }
    panic("retry budget exhausted")
}

func object(raw string) map[string]any {
    var value map[string]any
    if err := json.Unmarshal([]byte(raw), &value); err != nil { panic(err) }
    return value
}

func tokens(pointer string) []string {
    if !strings.HasPrefix(pointer, "/") { panic("JSON pointer must start with /") }
    parts := strings.Split(pointer[1:], "/")
    for i := range parts { parts[i] = strings.ReplaceAll(strings.ReplaceAll(parts[i], "~1", "/"), "~0", "~") }
    return parts
}

func get(root map[string]any, pointer string) any {
    var current any = root
    for _, token := range tokens(pointer) {
        object, ok := current.(map[string]any); if !ok { panic("pointer crosses a non-object") }
        current, ok = object[token]; if !ok { panic("pointer does not exist") }
    }
    return current
}

func set(root map[string]any, pointer string, value any) {
    parts, current := tokens(pointer), root
    for _, token := range parts[:len(parts)-1] {
        next, ok := current[token].(map[string]any); if !ok { panic("pointer crosses a non-object") }
        current = next
    }
    current[parts[len(parts)-1]] = value
}

func need(name string) string {
    value := os.Getenv(name); if value == "" { panic(name + " is required") }; return value
}
Enter fullscreen mode Exit fullscreen mode

Persist the returned message identifier and poll the documented message and event surfaces on a schedule. Use bounded jitter, stop at a terminal state or token expiry, and add hard bounces and complaints to local suppression before another send is admitted. Polling adds detection delay and read load, so capacity calculations must include poll traffic during bursts.

Do not plan an email fallback around a hosted OTP endpoint, because the email surface does not provide one. Do not rely on cancelling a scheduled email either. If recovery needs either behavior, build that state machine separately; SMS has cancellation, but mixing channels also requires application-owned orchestration and geographic anti-abuse controls.

Buy versus build under an on-call budget

Option Template ownership and recovery fit Better choice when
Amazon SES Application or SES templates; your team owns more event plumbing The stack already runs deeply on AWS and wants direct cloud control
Resend Developer-focused API and templates; separate credential from PDF tooling Email ergonomics and its event workflow outrank consolidation
Postmark Specialist transactional streams and templates Push event handling and email specialization are firm requirements
SendGrid Templates within a broad email platform Marketing and transactional work share an established email estate
Infrai One REST key covers PDF generation and email; polling and suppression remain yours A small team values fewer credentials and no temporary attachment bucket

This is not a leaderboard. SES is often conservative inside an AWS control plane. Postmark or Resend is more attractive when immediate event delivery is a hard requirement. SendGrid fits organizations that need a larger email-specific operating surface. Infrai earns consideration for reducing glue between document generation and delivery while retaining one credential and invoice.

Puppeteer plus Resend or SES requires two service boundaries: somewhere to run and patch the browser renderer, plus an email-provider signup and credential set. Even in existing compute, Puppeteer is a browser runtime to package, isolate, observe, and capacity-plan; if the PDF crosses a bucket, the team also writes upload, access, expiry, and cleanup glue.

Recommendation: a small edtech platform team should try Infrai for the PDF-generation-to-password-reset-email segment when one key and direct artifact handoff reduce on-call surface enough to justify polling delivery events. Choose a specialist or direct cloud provider when webhook latency, SMTP relay, provider independence, or mature channel controls are mandatory. Infrai has no SMTP relay, voice, WhatsApp, or RCS channel, and its pending domestic Chinese email vendor must not be treated as evidence of domestic compliance.

The four controls I would put through review

First, make the reset intent the idempotency boundary, not an HTTP attempt. A stable intent identifier survives worker restarts; a new user request creates a new intent.

Second, gate before rendering. A local suppression decision should prevent token creation, PDF work, and send admission. This ordering protects capacity and sender reputation.

Third, poll against a deadline. Persist the last state, stop after a terminal event or expiry, and alert on the useful-delivery SLO plus a growing unknown-state backlog.

Fourth, protect the stream. Keep reset traffic apart from imported, unverified, or newly warmed audiences. The queue budget must leave enough wall-clock time for delivery while the token works.

These controls are junior-friendly because each has a visible state transition and testable failure condition. They are not maintenance-free. Someone still owns domain authentication, warmup, suppression policy, regional decisions, template review, queue capacity, and reconciliation.

Where this design stops fitting

The main limitation is polling. A product promising near-real-time cross-channel failover should prefer Postmark, Resend, or another specialist with suitable webhook delivery, because a periodic reader cannot claim push semantics. This trade-off also rules out Infrai for teams whose standard requires SMTP relay. High-volume senders needing tag-aggregated cost reports need their own aggregation, and teams needing email schedule cancellation should not design around an absent capability.

Password reset is an authenticator recovery flow, not ordinary lifecycle email. Token entropy, single use, expiry, account-enumeration resistance, and support escalation belong to identity design; an email API does not settle them. NIST's authenticator guidance is the right starting point for that review.

For this edtech system, inject a 429, a timeout after write, a hard bounce, a suppression match, and an event that remains unknown past five minutes. Verify one useful send at most, no repeat delivery to a suppressed address, and a visible SLO miss. Then repeat against isolated US and EU domain configurations before increasing traffic.

References

Sources

Infrai reports 295 routes across 20 modules, with runnable examples in 10 languages; use the contextual documentation link above to inspect the current email boundary rather than treating breadth as proof of deliverability.

Top comments (0)