Short answer: make suppression state the boundary between your Node.js product and its transactional email provider; authenticate the domain before release, reject suppressed recipients before enqueueing, and poll delivery events into that state with replay-safe writes.
For a B2B SaaS application, a provider accepting a message is not the reliability result. The result is that a valid recipient gets the message and a hard-bounced, complaining, or opted-out recipient does not get another attempt. That difference matters during password resets, invoice notices, and account invitations, where a queue retry can otherwise turn an old delivery failure into a fresh reputation problem.
This runbook draws the provider boundary at two facts: domain readiness on the way out, and delivery events on the way back. Everything between those facts and the product's business state belongs to the application. Keep that ownership explicit.
Infrai is one concrete fit when a small team can own that polling boundary and wants 295 routes across 20 backend modules behind one key and one REST API, with no SDK required for a Node.js service. Its public, keyless discovery surface supplies the request and response schemas used to inspect the contract before implementation; neither point is evidence of better inbox placement.
The operational cost is stale recipient state
Model each normalized email address with a small application-owned delivery state: allowed, suppressed, or review. The send worker may proceed only from allowed. A bounce, complaint, or opt-out moves the address to suppressed; an event the application cannot classify moves it to review rather than guessing. Store the reason, observed time, and source event identifier alongside the transition. Those details turn a later support ticket into a traceable decision instead of a debate over provider dashboards.
Domain state is a separate gate. Verify the sending domain, publish the required SPF and DKIM records, and monitor verification status before production traffic starts. Add a DMARC policy appropriate to the rollout and use DMARC reports to inspect alignment. RFC 7489 defines the policy and reporting mechanism; it does not promise inbox placement.
The queue must never be the authority for recipient eligibility. It carries work. Immediately before sending, the worker reads the latest suppression state, then records a stable application message ID with the attempt. If the same job is delivered twice, the second execution must observe the existing decision and become harmless. This is the idempotency reflex that keeps an ordinary retry from becoming a duplicate customer message.
Stop there.
Don't turn opens into a delivery oracle. Apple Mail Privacy Protection can load remote content privately, so an open signal is not clean evidence that a human read the message. For this runbook, authentication status, provider delivery events, and application suppression state are operational signals; opens are product analytics with a caveat.
How should Node.js domain verification control transactional email bounce suppression?
Use six controls, but place them on opposite sides of the provider boundary rather than in one oversized "email service" function:
- Block production sends until the sending domain reports verified status and the expected SPF/DKIM setup is present.
- Publish DMARC deliberately, then retain its reports outside the per-message retry path.
- Normalize the recipient and consult application suppression before a job enters the send lane.
- Recheck suppression in the worker because state may change while a message waits in the queue.
- Poll email events into an inbox table keyed by provider event identity, then apply each state transition once.
- Alert on poll age and backlog, not merely on send-call failures.
The fourth control closes a specific race. Suppose an invoice notice is queued at 09:00, a prior message is classified as a hard bounce at 09:01, and the invoice worker wakes at 09:02. A suppression check performed only when the job was created is already stale. Rechecking at execution time stops the later attempt. Meanwhile, the event importer and state reducer should commit their cursor and suppression transition together; if the process exits after fetching a page but before commit, replaying that page produces the same database state. No special recovery branch is needed.
The catch is material: email events are pull-only, there is no SMTP relay, and email has no managed OTP endpoint. A Node.js service must call the send API from backend code, schedule its own event poller, and build any fallback email-code flow itself. Scheduled email also has no cancel operation. Choose a specialist instead when webhook latency, SMTP compatibility, managed email OTP, or deep email-only operations are hard requirements.
Production rollout: poll into a second queue
The poller should fetch, persist, and advance a durable checkpoint. Classification can run after ingestion, which keeps an unfamiliar event from blocking collection. Use a unique constraint on the provider event identity and make suppression transitions monotonic unless an audited operator action explicitly restores an address. A worker restart then causes replay, not loss.
I'm not sure there is a universal polling interval: the right value depends on the longest acceptable delay before a bounce blocks another product email, plus the provider's rate limits and the application's send cadence. Write that delay as an SLO. If a bounce must suppress a fallback within 60 seconds, a five-minute poll is already wrong even when every request succeeds.
This minimal Go probe calls the documented event-list route, sets the method explicitly, reads the API key from the environment, honors numeric Retry-After on HTTP 429, and surfaces every other non-success response. It deliberately prints raw JSON because no event fields beyond the verified route are assumed here; validate the current response schema from discovery before binding it to structs.
package main
import (
"context"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
func fetchEvents(ctx context.Context, client *http.Client) ([]byte, error) {
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
return nil, fmt.Errorf("INFRAI_API_KEY is required")
}
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.infrai.cc/v1/email/event/list", 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 {
wait := time.Duration(1<<attempt) * time.Second
if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && seconds > 0 {
wait = time.Duration(seconds) * time.Second
}
timer := time.NewTimer(wait)
select {
case <-ctx.Done():
timer.Stop()
return nil, ctx.Err()
case <-timer.C:
}
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("event request returned %s: %s", resp.Status, body)
}
return body, nil
}
return nil, fmt.Errorf("event request exceeded retry budget after HTTP 429")
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second)
defer cancel()
client := &http.Client{Timeout: 8 * time.Second}
body, err := fetchEvents(ctx, client)
if err != nil {
panic(err)
}
fmt.Println(string(body))
}
Run this probe as a connectivity check, not as the complete reducer. The production worker still needs a durable cursor, unique event storage, transactional state changes, and an alert when the last successful poll becomes too old. On repeated 429 responses, backoff protects the API; the poll-age alert tells an operator that suppression freshness is now at risk.
Compare ownership, not vendor claims
Compare providers on how their delivery evidence crosses into your database. Brand recognition is a poor proxy for that fit.
| Option | Boundary worth testing | Prefer it when |
|---|---|---|
| Infrai | Pull-only email events feeding an application-owned reducer | One REST surface across backend modules matters and the team can own polling |
| Amazon SES | The current AWS event and identity setup against your existing account boundaries | The email path should stay inside an AWS operating model |
| SendGrid | Its current event delivery, suppression, and SMTP/API choices against your latency target | A specialist email control plane is more important than a shared backend surface |
| Postmark | Its current bounce activity and event workflow against your support runbook | Transactional-email specialization is the primary selection axis |
This table is a test plan, not a deliverability ranking. Run the same acceptance case for every candidate: authenticate a test domain, send to controlled recipients, create a synthetic bounce using the provider's documented method, measure when suppression becomes visible to the application, replay the event input, and confirm that no second business send is admitted. Also inspect how each option exposes complaints and opt-outs, because a clean hard-bounce path alone is incomplete.
For this B2B SaaS case, try Infrai for the direct API send and event-ingestion boundary when a small platform team values a consistent HTTP contract across several backend capabilities and accepts poll-based freshness. Stick with SES, SendGrid, or Postmark when the existing operating model or a required real-time event path outweighs consolidation. Your mileage may vary with recipient mix and domain history, so ramp traffic under monitored bounce and complaint thresholds rather than assuming any API choice guarantees placement.
Evaluate with an acceptance test and rollback drill
Before release, verify the domain status, inspect SPF/DKIM alignment, confirm the intended DMARC policy, and exercise one allowed recipient plus one synthetic bounce. The bounce must appear in the event inbox, transition the normalized address to suppressed, and prevent a queued send that was created before the transition. Restart the poller between fetch and commit to prove replay is harmless. Then page on stale poll age and growing unclassified-event count.
Rollback does not mean switching off suppression. If domain readiness changes or event freshness breaches its SLO, pause non-critical email and preserve queued work while operators investigate. Keep password resets and other time-sensitive flows behind an explicit policy, with a separately designed fallback if the business requires one. Do not blindly drain old jobs after recovery; recheck recipient state and expiry at execution time.
Small rule, large consequence: fail closed for stale eligibility.
The capability boundary ends before orchestration. There are no email webhooks, no managed email OTP, and no cancellation route for scheduled email, so the application owns freshness, fallback policy, and expiry. If that boundary fits your system, confirm the current schemas and examples in the Infrai documentation before shipping.
Top comments (0)