Short answer: the simplest reliable setup for a small SaaS is a transactional sender with custom-domain authentication, a deliberately paced warmup, and one replayable feedback poller that owns the suppression list. Keep the report generator and attachment policy in your application; let the sender transport bytes and report outcomes.
The page usually arrives too late. A marketplace report was generated, the email job was acknowledged, and a seller says nothing arrived. The on-call view shows a green queue depth and no obvious exception. The missing signal is often a complaint or hard bounce that never became a durable policy decision.
It wakes me.
I work backwards from that page. The send record needs a message id, recipient, domain, attachment checksum, and attempt count. The feedback record needs the provider event id, event type, observed time, and cursor. With those fields, a replay is an ordinary operation instead of a forensic exercise.
How should a small SaaS design custom-domain warmup, suppression, bounce, complaint tracking, and a polling API?
Warmup is a traffic control problem. Publish SPF, DKIM, and DMARC for the sending domain, align the visible From identity with the authenticated identity, then increase legitimate transactional volume in steps. SPF defines authorized senders; RFC 7208 does not promise inbox placement. Keep a separate denominator for accepted, deferred, hard-bounce, and complaint events.
For a generated marketplace report, the first step can be one report per seller cohort, followed by a review of bounce and complaint rates before the next step. Do not mix password resets, receipts, and bulk announcements in the same decision window. Their recipient expectations differ, so one aggregate rate hides the failure you need to see.
Suppression is a policy table, not a provider dashboard. A hard bounce or complaint adds a recipient to the table before the next send is rendered. A later delivered event does not silently remove that row. Re-enablement requires a named, audited action.
The least surprising polling contract is an opaque cursor with stable event ids. Poll a page, validate it, apply idempotent state changes, commit the state and cursor together, and only then acknowledge the page. That ordering is what prevents a restart from skipping a complaint.
The alert-to-action trace: what should the on-call page actually prove?
Start with the alert fired at 09:12: “report delivery lag above 10 minutes for three consecutive polls.” The on-call checks queue age, then the last committed feedback cursor, then the suppression-write transaction. A page that says only “email provider unhealthy” is not actionable; it cannot distinguish a stalled poller from a domain-authentication change.
The earlier signal should have been a rising gap between provider event time and suppression commit time. Instrument both timestamps, plus poll duration, page size, duplicate-event count, and the number of sends rejected before rendering. Alert on the gap and on cursor age. A high duplicate count is usually a replay or retention problem, not proof that the sender delivered twice. In one review I would reconstruct the whole path from a single report id: the worker records the attachment checksum, hands the message to the sender, and stores the provider message id; the poller later sees a deferred event, then a hard bounce, and attempts the suppression upsert. If the cursor is six pages behind, the operator can replay those pages without resending anything because the send path checks the policy table before it renders. If the cursor is current but the suppression commit timestamp is old, the database transaction or worker pool is the suspect. If both are current and the seller still has no report, inspect the recipient's mailbox rules and attachment limits. That is a useful page because each branch names a next action and leaves an audit trail. A dashboard full of green transport counters cannot provide that chain.
No shortcut.
Thresholds have a cost. Set them too low and a short provider delay wakes someone during every traffic burst; set them too high and a complaint can pass through the send path for hours. I am not sure one threshold travels across every recipient mix, so record the time window, owner, and reason in the runbook and revisit it after a real warmup cohort.
A Go poller that makes replay safe
The following interface leaves transport details outside the worker. It is intentionally small: the application can adapt a hosted API, a self-hosted relay, or a standards-based mailbox feed without changing suppression semantics.
package feedback
import "context"
type Event struct {
ID string
Recipient string
Kind string
SeenAt string
}
type Page struct {
Events []Event
Next string
}
type Source interface {
Poll(ctx context.Context, cursor string) (Page, error)
}
type Store interface {
Apply(ctx context.Context, event Event) error
CommitCursor(ctx context.Context, cursor string) error
}
func Drain(ctx context.Context, source Source, store Store, cursor string) (string, error) {
page, err := source.Poll(ctx, cursor)
if err != nil {
return cursor, err
}
for _, event := range page.Events {
if event.Kind == "hard_bounce" || event.Kind == "complaint" {
if err := store.Apply(ctx, event); err != nil {
return cursor, err
}
}
}
if page.Next == "" {
return cursor, nil
}
if err := store.CommitCursor(ctx, page.Next); err != nil {
return cursor, err
}
return page.Next, nil
}
Apply must upsert by event id and recipient, so replaying a page is harmless. In a relational store, put the event insert, suppression upsert, and cursor update in one transaction. If the process dies before the commit, the page is read again. If it dies after the commit, both policy state and cursor are already durable.
I test this with a duplicate page and an out-of-order sequence: deferred at 09:04, complaint at 09:06, then the same complaint at 09:06:30. The seller report remains blocked, the event is stored once, and the cursor advances once. Small detail. Important detail.
Which provider boundary fits the reliability requirement?
Compare contracts rather than screenshots. Amazon SES exposes transport and event-publishing primitives, so the application carries more policy. SendGrid offers managed templates and an event webhook, which can reduce setup while moving copy review into another console. Postmark centers transactional streams and message events, a useful boundary when marketing traffic is out of scope. These are trade-offs, not a ranking.
Ask each candidate for four answers: how long are bounce and complaint events retained; can a cursor be replayed; which identity signs the custom domain; and can the team export an immutable template version? Also ask how attachment size limits are surfaced before a send is accepted. A polling API that answers those questions is more valuable than a long endpoint catalog.
The catch is that polling is not suitable when a complaint must block a send within seconds or when the provider retains events for less time than your maximum outage window. Choose push delivery or a longer-retention event store in that case. A hosted template editor is also a poor fit when every copy change requires a pull request and localization review; keep the template in code and pin its version instead.
Cost can be one input, but it should not be the decision rule. Reliability comes from authenticated domains, explicit suppression state, replay tests, and alerts tied to action. Your mileage may vary by recipient mix and attachment volume, so measure event-to-suppression lag during warmup before committing to a boundary.
The practical rule is simple: own the security and attachment promise, outsource the transport if it helps, and make every feedback event replayable. That leaves a small marketplace SaaS portable when its sender or volume changes.
References
- https://datatracker.ietf.org/doc/html/rfc7208
- https://www.rfc-editor.org/rfc/rfc6376
- https://www.rfc-editor.org/rfc/rfc7489
- https://docs.aws.amazon.com/ses/latest/dg/monitor-sending-activity.html
- https://www.twilio.com/docs/sendgrid/ui/sending-email/event-webhook
- https://postmarkapp.com/developer/webhooks/webhooks-overview
Top comments (1)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.