Cloud & Infrastructure · Email Infrastructure
Transactional Email Engineering: Why Your Emails Land in Spam and How to Fix It
Password resets, invoices, and notification emails are infrastructure. Most developers treat them as an afterthought until a client asks why their welcome emails are disappearing. Here is the full picture.
Anurag Verma
8 min read
Sponsored
A client emails you six weeks after launch: “Our users aren’t getting password reset emails.” You check the logs. The emails sent. The provider reported delivery. The user never received them. It went to spam, and now the client is losing signups because of it.
This happens because transactional email looks simple (just call an API and send a string) but actually has a series of authentication, reputation, and configuration requirements that are easy to skip and painful to fix after the fact. Here is what those requirements are and how to handle them before they become a production incident.
The Three Authentication Records That Matter
Email spam filtering is not just about content. The receiving server checks whether the sending domain actually authorized this email to be sent. Three DNS records handle this.
SPF (Sender Policy Framework) lists the IP addresses and mail servers authorized to send email for your domain.
TXT "v=spf1 include:sendgrid.net include:_spf.google.com ~all"
The ~all at the end is a soft fail: emails from unauthorized sources are flagged but not rejected. The -all is a hard fail. Start with ~all, switch to -all after you’re confident the record covers all your sending sources.
DKIM (DomainKeys Identified Mail) signs outgoing emails with a private key. The receiving server fetches the corresponding public key from your DNS and verifies the signature. This proves the email wasn’t tampered with in transit and that it came from someone who controls your domain.
Your email provider generates the key pair and gives you a DNS record to add:
TXT selector._domainkey.yourdomain.com
"v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb..."
The selector is a string chosen by your provider (Postmark uses 20190101, Resend uses resend, etc.). The record value is the base64-encoded public key.
DMARC (Domain-based Message Authentication, Reporting, and Conformance) ties SPF and DKIM together and tells receiving servers what to do when either check fails.
TXT _dmarc.yourdomain.com
"v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.com; pct=100"
Start with p=none, which monitors without taking action. After two to four weeks of receiving DMARC reports and confirming your legitimate email is passing, move to p=quarantine (spam folder for failures) and then p=reject (block failures entirely). Jumping straight to p=reject before your records are clean will lose legitimate email.
Choosing a Provider and Not Choosing Wrong
The four providers worth considering for transactional email in 2026:
| Provider | Starting price | Shared IP pool | Dedicated IPs | Webhook events |
|---|---|---|---|---|
| Resend | Free / $20/mo | Shared | $30/mo | Yes |
| Postmark | $15/mo | Shared | $50/mo | Yes |
| AWS SES | $0.10/1000 | Shared | $24.95/mo | Yes |
| SendGrid | $19.95/mo | Shared | $89.95/mo | Yes |
The price comparison is misleading without one clarification: shared IP pools mean your deliverability depends partly on other senders using the same IPs. If a bad actor on your provider’s shared pool spams aggressively, your IPs get flagged and your legitimate email suffers.
For most applications sending under 10,000 emails per month, shared IPs from a reputable provider are fine. Postmark and Resend are known for strict anti-spam enforcement on their shared pools, which keeps IP reputation high. SES shared pools are less curated.
Dedicated IPs are worth considering once you’re sending more than 50,000 emails per month consistently. Below that volume, dedicated IPs often have worse deliverability than shared pools because they lack the sending history to establish reputation.
Warming Up a New IP
A new dedicated IP has no sending history. Gmail, Outlook, and other providers trust IPs with established reputation and distrust new ones. Sending 50,000 emails immediately from a cold IP will land most of them in spam.
Warming up means gradually increasing send volume over four to six weeks:
| Week | Daily volume |
|---|---|
| 1 | 200-500 |
| 2 | 1,000-2,000 |
| 3 | 5,000-10,000 |
| 4 | 20,000-50,000 |
| 5-6 | Target volume |
During warmup, send only to engaged users, meaning people who have opened or clicked in the past 90 days. High engagement rates during warmup build reputation faster. Sending to a cold list of emails that ignore or mark you as spam during warmup poisons the IP before it’s established.
Shared IP pools skip warmup because the IP already has history. This is another reason most applications should use shared pools until volume justifies dedicated IPs.
The Technical Setup: Resend as an Example
Resend is the simplest provider to integrate for developers and worth using as a reference for the general pattern.
Add the domain in the Resend dashboard and add the DNS records they give you: one DKIM record, optional SPF update if you’re not already using another provider. Resend handles DMARC alignment automatically.
// Using the Resend SDK
import { Resend } from 'resend'
const resend = new Resend(process.env.RESEND_API_KEY)
async function sendPasswordReset(to: string, resetToken: string) {
const { data, error } = await resend.emails.send({
from: 'no-reply@yourdomain.com',
to,
subject: 'Reset your password',
html: passwordResetTemplate(resetToken),
tags: [{ name: 'category', value: 'transactional' }],
})
if (error) {
// Log the error but don't throw. Email failure shouldn't crash the auth flow
logger.error('Failed to send password reset email', { to, error })
return false
}
return data?.id
}
Two things to note: the from address uses your verified domain, not a free email. Gmail and Outlook flag email from @gmail.com sent via an API with low trust. Use your own domain.
The tags field categorizes email for tracking in the Resend dashboard. This is useful when you’re sending multiple types (transactional, marketing) and want to track deliverability separately.
Webhook Integration for Bounce and Complaint Handling
Every provider emits webhook events for delivery status. You need to handle them, or your list will fill with invalid addresses that damage your reputation.
The events that matter:
- Bounce (permanent): The address doesn’t exist. Remove it from your sending list immediately.
- Bounce (transient): Delivery failed temporarily. Retry later, but remove after several transient bounces.
- Complaint: The recipient marked the email as spam. Remove and never email again.
- Unsubscribe: Legal in some jurisdictions to keep sending transactional email, but respect the preference.
// Express webhook handler
app.post('/webhooks/email', async (req, res) => {
const event = req.body
switch (event.type) {
case 'email.bounced':
if (event.data.bounce_type === 'hard') {
await db.emailAddresses.update({
where: { address: event.data.to },
data: { status: 'bounced', bouncedAt: new Date() },
})
}
break
case 'email.complained':
await db.emailAddresses.update({
where: { address: event.data.to },
data: { status: 'complained', complainedAt: new Date() },
})
break
}
res.sendStatus(200)
})
Before sending any email, check that the address isn’t marked as bounced or complained. Continuing to send to bad addresses after you know about them is what turns a temporary reputation issue into a permanent one.
Monitoring Deliverability
DNS records being correct doesn’t guarantee good deliverability. Monitoring gives you visibility before a client notices.
Google Postmaster Tools is free and tracks domain reputation, spam rate, and delivery errors specifically for email going to Gmail accounts. If your spam rate climbs above 0.3%, Gmail starts routing more of your email to spam. Above 0.1% sustained, investigate immediately.
MXToolbox lets you check whether any of your sending IPs are on known blocklists. A spot on a blocklist explains sudden deliverability drops.
DMARC reports: The rua address in your DMARC record receives XML reports from receiving servers about how your email is being processed. Most of these are unreadable as raw XML. Services like Postmark’s DMARC Digests, Valimail, or dmarcian parse them into something useful.
The From Address and Display Name
Small things that affect spam filtering more than most developers expect:
A consistent from address builds sender reputation. Rotating between hello@, noreply@, and notifications@ splits reputation across addresses and resets trust-building.
no-reply@yourdomain.com is common but subtly bad for deliverability: it signals to spam filters that you’re not expecting responses, which correlates with bulk sender behavior. hello@yourdomain.com or notifications@yourdomain.com that actually accepts replies performs slightly better.
The display name should match what users expect. If they signed up with “Acme App,” the from should read Acme App <hello@acme.com> not System Notifications <noreply@acme.co>. Mismatched sender names increase spam reports.
Where Things Tend to Go Wrong on Deployed Projects
The most common failure modes on projects that looked fine in development:
Missing SPF after adding a new sending source. You set up SPF for Resend, then add a new service that sends email notifications, and forget to add it to the SPF record. Emails from the new service fail SPF and start landing in spam.
Sending from subdomains without separate DNS records. SPF and DKIM records on yourdomain.com don’t automatically apply to app.yourdomain.com or mail.yourdomain.com. Each subdomain needs its own records.
Not handling bounces. The initial launch goes fine, but six months later 8% of the email list has churned (people change jobs, delete accounts, etc.). Sending to those addresses generates hard bounces that accumulate and damage reputation.
Using a free email domain as the from address in production. Works in development because your own test account accepts anything. Fails in production because the provider’s from-address validation is different, or because Gmail applies stricter rules to email claiming to be from Gmail but arriving via an API.
Set up all three DNS records before launch, integrate bounce handling before you have a list worth protecting, and monitor Postmaster Tools from day one. None of this is difficult. It’s easy to skip and expensive to fix once the reputation is damaged.
Sponsored
More from this category
More from Cloud & Infrastructure
Multi-Cloud vs Single Cloud in 2026: An Honest Cost-Benefit Analysis
OpenTelemetry for Web Apps in 2026: What to Instrument and What to Skip
KEDA, VPA, and Goldilocks: Kubernetes Autoscaling Beyond the HPA in 2026
Sponsored
The dispatch
Working notes from
the studio.
A short letter twice a month — what we shipped, what broke, and the AI tools earning their keep.
Discussion
Join the conversation.
Comments are powered by GitHub Discussions. Sign in with your GitHub account to leave a comment.
Sponsored