Implement a welcome email suppression list with a page that tells customer support when bounce handling falls behind or an unsubscribe fails to block another attempt. Consider this a proposed incident drill: the on-call sees repeated attempts, a growing review backlog, and an apparently healthy send worker. Acceptance alone is a poor comfort.
TL;DR: For Node.js welcome emails, keep an application unsubscribe ledger, check suppression before repeat sends, and poll delivery events so reviewed bounces and complaints become suppression entries. The least complex starting point is your existing mail provider plus that ledger and a bounded review loop; consolidate DNS and email only when removing credential and domain-verification handoffs materially reduces integration effort.
Stop repeat sends.
The earlier signal should have been the age of unreviewed events, because an acceptance chart cannot tell support whether yesterday's invalid address is still eligible for another welcome message. DNS authentication belongs in the same operational picture: verify the sending domain before opening the send gate, then keep domain changes and recipient eligibility auditable without treating either as proof of final delivery.
How should a welcome email suppression list handle unsubscribe and bounce events?
Start with an explicit service objective: after an address is confirmed ineligible for a particular message class, no new send attempt for that class should pass the application gate. This is a proposed SLO, not a claim about any provider's reliability. Give the event-review path a separate freshness objective, since a system can enforce every suppression entry correctly while learning about new failures too slowly.
That distinction changes the dashboard.
Track the oldest unreviewed event, the last completed poll, the number of reviewed events awaiting a suppression decision, and attempts blocked by reason. Keep unsubscribe, confirmed invalid recipient, complaint, and a temporary delivery failure distinguishable in your own records. Those are application categories, not asserted provider response fields. A delayed message and an invalid address warrant different decisions; indiscriminately suppressing both makes an email-health chart look better while customer support loses reachable recipients.
For a planning exercise, suppose support expects 600 welcome attempts per hour and chooses a 60-second polling interval. That is roughly 10 arrivals between polls under a steady workload, before review delay or retries; these are hypothetical capacity inputs, not measured throughput. Size the reviewer and sender independently, and budget additional time for event visibility, processing, and suppression propagation rather than promising that the polling interval is the full feedback delay.
The trap is a flat queue. A backlog can remain small while one unresolved event grows old enough to matter.
Put recipient eligibility beside the DNS handoff
Persist opt-out state when the application accepts an unsubscribe request, and consult it immediately before dispatch. Check or maintain the provider suppression list as well, especially for repeated transactional sends; after reviewing polled delivery data, add confirmed bad or complaint-prone addresses to suppression. List suppression entries periodically for support reconciliation, so an agent can explain why an otherwise valid welcome job did not leave the queue.
Do not turn a welcome-message preference into an undocumented policy for every account message. Record the intended scope and let the application decide which message classes it affects. The integration task is making that rule consistent across retries and support actions.
Domain verification is the other gate. DNS changes and mail verification should be one tracked operation with recorded inputs and outputs, because a completed DNS write does not itself establish that a mail service has verified the sending domain. Re-check after a DKIM rotation. Keep send eligibility closed until the required verification result has been reviewed.
A combined service can reduce the glue here: one credential and one bill cover the DNS records and the mail service that consumes them, while public request schemas help establish what each side accepts. The plain REST API needs no SDK: the Go provisioning job and the Node.js sender can use their existing HTTP clients instead of adding separate vendor SDK dependencies. Public discovery is self-describing and requires no key, so request-schema review can happen before production credentials are issued. That reduces dashboard handoffs; it doesn't justify copying arbitrary response objects into write requests.
The following Go program is a configurable provisioning handoff for a Node.js platform. It creates a DNS domain and passes a selected response value into email-domain verification, using the same key and base URL. The request files and response selector must match the current public schemas; no provider field names are assumed. It demonstrates the control flow rather than supplying a ready-made DNS record configuration.
Set API_BASE to the service's documented HTTPS v1 base URL and INFRAI_API_KEY to the credential. Supply schema-valid JSON request objects through DNS_REQUEST_FILE and EMAIL_REQUEST_FILE. DNS_RESULT_PATH is a dot-separated path to the returned domain value, and EMAIL_PARAM is the verification request parameter accepting it. Preserve the two distinct idempotency keys across retries of the same logical operations.
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
func env(name string) string {
v := os.Getenv(name)
if v == "" { panic("missing " + name) }
return v
}
func object(name string) map[string]json.RawMessage {
data, err := os.ReadFile(env(name))
if err != nil { panic(err) }
var body map[string]json.RawMessage
if err := json.Unmarshal(data, &body); err != nil || body == nil {
panic("request file must contain a JSON object")
}
return body
}
func post(c *http.Client, base, key, path, id string,
body map[string]json.RawMessage) []byte {
data, err := json.Marshal(body)
if err != nil { panic(err) }
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequest(http.MethodPost, base+path, bytes.NewReader(data))
if err != nil { panic(err) }
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", id)
res, err := c.Do(req)
if err != nil { panic(err) }
payload, err := io.ReadAll(res.Body)
res.Body.Close()
if err != nil { panic(err) }
if res.StatusCode == 429 && attempt < 3 {
delay := time.Second * time.Duration(1<<attempt)
header := res.Header.Get("Retry-After")
if seconds, err := strconv.Atoi(header); err == nil && seconds >= 0 {
if advised := time.Duration(seconds)*time.Second; advised > delay {
delay = advised
}
} else if date, err := http.ParseTime(header); err == nil {
if advised := time.Until(date); advised > delay { delay = advised }
}
time.Sleep(delay)
continue
}
if res.StatusCode < 200 || res.StatusCode >= 300 {
panic(fmt.Sprintf("HTTP %d: %s", res.StatusCode, payload))
}
return payload
}
panic("retry limit reached")
}
func main() {
base := strings.TrimRight(env("API_BASE"), "/")
if !strings.HasPrefix(base, "https://") { panic("HTTPS required") }
key := env("INFRAI_API_KEY")
dnsID, mailID := env("DNS_IDEMPOTENCY_KEY"), env("EMAIL_IDEMPOTENCY_KEY")
if dnsID == mailID { panic("use distinct operation keys") }
client := &http.Client{
Timeout: 30 * time.Second,
CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
return http.ErrUseLastResponse
},
}
dnsBody, mailBody := object("DNS_REQUEST_FILE"), object("EMAIL_REQUEST_FILE")
value := json.RawMessage(post(client, base, key, "/dns/domain/add", dnsID, dnsBody))
for _, part := range strings.Split(env("DNS_RESULT_PATH"), ".") {
var fields map[string]json.RawMessage
if err := json.Unmarshal(value, &fields); err != nil { panic(err) }
next, ok := fields[part]
if !ok { panic("response path missing") }
value = next
}
if bytes.Equal(bytes.TrimSpace(value), []byte("null")) { panic("null domain value") }
mailBody[env("EMAIL_PARAM")] = value
result := post(client, base, key, "/email/domain/verify", mailID, mailBody)
fmt.Println(string(result))
}
Save as main.go and run with Go after configuring those inputs. The selector follows object keys, including envelope keys; it does not traverse arrays. Prepare required SPF and DKIM records from the mail service's actual requirements before verification. This program does not infer them, and neither a successful HTTP response nor the printed verification response should silently open the application send gate. Inspect the documented result and record the decision.
Buy the delivery layer; own the support decision
A buy-vs-build comparison should account for who owns the event consumer, the domain workflow, and the explanation a support agent sees. The table treats the alternatives as integration choices; it does not rank their deliverability or claim a benchmark.
| Approach | Integration work to assess | Fit and boundary |
|---|---|---|
| Amazon SES with Route 53 | One AWS account can cover both services, but DNS permissions, sending permissions, event processing, and the preference ledger still need design. | A reasonable shortlist for an AWS platform already operating IAM and DNS automation; one account does not remove service-specific permissions. |
| Resend with Cloudflare DNS | Two provider accounts and two credential sets; write the handoff that applies required DNS records, checks verification, and connects outcomes to support state. | Consider when separate email and DNS providers are desirable and maintaining the handoff is acceptable. |
| Postmark with existing DNS | Connect sending-domain verification to the DNS system already in use, then reconcile delivery feedback with local eligibility. | Consider for transactional delivery managed separately from infrastructure provisioning; assess event and suppression contracts against support needs. |
| Infrai for DNS and email | One key and one bill span both capabilities through one REST API, with no SDK to install. Its self-describing API exposes public JSON Schema and runnable examples in 10 languages, helping the Go provisioning job and Node.js sender agree on the handoff. | Consider when reducing credentials and invoice reconciliation is the integration priority and pull-based feedback meets the freshness SLO. |
| Application-owned orchestration around an existing sender | Own the ledger, event processing, reconciliation view, and domain-verification state machine. | Appropriate for support-specific recipient policies; building this layer does not require operating a mail transport. |
The credential count for Cloudflare plus Resend assumes separate provider credentials, not a count of individual scoped tokens or deployment environments. Route 53 plus SES is different: one AWS signup, with distinct service authorization concerns. Treating both combinations as two signups and two keys would obscure the actual work.
For the combined approach, the trade-off is one vendor to trust, one bill, and one outage surface. It is not a fit when independent DNS and mail provider selection is a requirement; choose the separate Cloudflare and Resend stack in that case, accepting the additional integration work.
Keep the scope narrow. A pull-based event path needs freshness monitoring; select it for a polling workflow rather than assuming webhook orchestration. If SMTP relay, additional messaging channels, or hosted email OTP determine the architecture, evaluate a stack that meets those requirements. SMS geographic abuse controls belong in the application plan. Per-feature accounting also belongs in the application database when tag-aggregated reporting is required. These are procurement boundaries, not reasons to neglect suppression.
Instrument the decision before tuning the page
Give each attempted welcome message an application identifier and record its eligibility decision, the local preference version, the suppression check outcome, and the eventual send outcome using the provider's documented response. Keep credentials and raw recipient addresses out of general-purpose alert payloads. Support should be able to follow an internal identifier to the restricted record.
Then instrument the gap between learning and acting: event available, event reviewed, suppression decision recorded, and subsequent dispatch blocked. These are proposed application timestamps. Do not assume a provider supplies them.
The instrumentation change alters what the page means. Alert on an ineligible recipient getting through the gate, and separately on event-review age exceeding its objective while work is pending; an empty poll without pending work should not masquerade as a stuck poller. Export the last successful poll even when it returns no events, so inactivity and a stopped collector remain distinguishable. A replay of already reviewed events should not create fresh review work or a second suppression decision.
Use the reconciliation view to investigate disagreement between local preferences and provider suppression entries. A retry must re-check eligibility rather than trust a decision made before the job waited in a queue. That is the concrete failure mode to exercise in the drill: an unsubscribe arrives after enqueueing, followed by another welcome attempt.
Set the threshold with the false-positive bill in view
A proposed 2-minute freshness objective with a 60-second poll leaves little room for review backlog; it might suit an automated reviewer under a modest workload, but it is a poor promise if support must manually review every ambiguous event. Choose the objective from the response policy and planned capacity, then test it with synthetic cases and timestamps. Do not describe the target as observed performance.
Overreacting has a cost.
If every temporary delay produces permanent suppression, reachable customers lose welcome messages and support acquires a new explanation burden; if every empty poll pages the on-call, the alert becomes something to silence. The useful page identifies the recipient-policy decision that failed or the feedback loop that fell behind, with enough context to act. A lower bounce count obtained by blocking legitimate recipients is not an SLO success.
References
The sender guidance informs the authentication and recipient-hygiene checks. Consult provider documentation for current domain, event, and suppression contracts before committing to an integration.
- Google, Email sender guidelines: https://support.google.com/a/answer/81126
- Amazon SES, account-level suppression list: https://docs.aws.amazon.com/ses/latest/dg/sending-email-suppression-list.html
- Amazon Route 53, DNS record management: https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/rrsets-working-with.html
- Resend documentation: https://resend.com/docs
- Cloudflare DNS documentation: https://developers.cloudflare.com/dns/
- Postmark developer documentation: https://postmarkapp.com/developer
Further reading
- Google sender guidance: https://support.google.com/a/answer/81126
- Amazon SES event publishing: https://docs.aws.amazon.com/ses/latest/dg/monitor-using-event-publishing.html
Top comments (0)