I Was Paying $20 a Month to Email Myself

The line item was $19.95. Every month, for a SendGrid Essentials plan whose entire job on my portfolio site was delivering contact-form submissions — to me. A visitor fills out the form, SendGrid carries the message from my server to my inbox, and for that privilege I was paying about $240 a year against a 50,000-email allowance I was using a few dozen sends of. If you have ever kept a vendor around because it already works and the bill is small enough to ignore, you know exactly how this happens. Nothing was broken. That was the problem.
This is the story of the wall that finally made me look at the bill, the one-evening migration to Resend that followed, and — more useful than either — the framework I now use to decide when vendor loyalty has quietly become an inertia tax.
The wall that forced the decision
I wasn't shopping for an email provider the night this happened. I was wiring up email capture: my site's sign-up flow fires a webhook, and the handler upserts the new subscriber's address into a mailing list. On SendGrid, contact storage lives in a product called Marketing Campaigns. I opened the contacts page to grab a list ID and hit a hard wall instead: "You don't have access to this page." My paid transactional plan didn't include contact storage. That's a separate product, with its own long-expired trial and its own bill.
Sit with the shape of that for a second, because the shape matters more than the vendor. I was already paying $19.95 a month. The feature I needed — storing an email address — meant paying for a second product on top of it. Meanwhile the actual work my paid plan performed was a rounding error against its limits.
That's the moment a switching decision gets cheap to see clearly: when the incumbent asks for more money to do something the alternative includes on its free tier. Resend includes contacts and segments on every plan. Its Pro tier — twenty dollars, functionally the same money I was already spending — covers 50,000 emails a month across ten domains. Same bill, several products' worth of headroom, and it was already the direction I'd chosen for future projects because of how it integrates with the rest of my stack.
The inertia audit: what was SendGrid actually doing?
Before you can migrate a vendor, you need an honest inventory of every thread connecting it to your codebase. Mine took ten minutes with grep and turned up exactly three touchpoints:
- The contact form route. An API route using
@sendgrid/mailto deliver form submissions to my inbox. - The sign-up capture webhook. The handler that had just hit the paywall, trying to PUT contacts into a marketing list.
- CMS transactional email. My site runs Payload CMS, which sends its own operational mail — password resets for the admin — through a nodemailer adapter speaking SMTP to SendGrid.
Three touchpoints is a small surface, and knowing that number changed everything about the decision. "Migrate off SendGrid" sounds like a project; "swap three call sites and verify a domain" sounds like an evening, and only one of those framings is real. You cannot price a migration you haven't scoped, and unscoped migrations always feel bigger than they are. That inflated feeling is half of what inertia is made of — the other half is the small bill that never quite hurts enough to schedule the work.
A framework: four signs the loyalty is just inertia
Cost alone doesn't justify a migration — my SendGrid bill was annoying, not painful. What justifies one is several signals stacking. Here's the checklist that made this decision for me, in roughly the order I'd weigh them:
1. You're paying for one feature and using a fraction of it. My usage was single-digit percentages of the plan's allowance, and the plan existed for a single function. When your bill buys headroom you will never grow into, you're not a customer — you're a donor.
2. A feature you now need is walled behind a second purchase. This is the classic incumbent move, and it's also the clearest price signal you'll ever get, because it lets you compare marginal cost directly: their upsell versus the alternative's included tier. When the alternative includes it free, the comparison does the deciding for you.
3. You've already chosen the alternative's direction. I had picked Resend for future projects before this evening ever happened — for its API design, its MCP server, and how it slots into the platform stack I'm consolidating on. When the strategic decision is already made, every month on the old vendor is rent paid on a building you've decided to leave.
4. You're in a cheap migration window. This one is underrated and time-sensitive. My site was weeks from promoting a rebuild to production. Production's email configuration didn't exist yet — meaning if I migrated staging now, production would be born on Resend and there would never be a production email cutover at all. Migration windows like this close, and they don't reopen.
Score your own vendor against those four. One signal is a grumble. Three or four is a decision you've already made and haven't admitted yet.
Worth naming the counter-signals too, because not every annoying bill deserves an evening. If you're actively using vendor-specific features that have no equivalent on the alternative, the migration has real scope you haven't priced yet. If you're days from a launch, the calendar wins — a migration window that overlaps a deadline isn't cheap, whatever the diff says. And if you have no way to verify the new path end-to-end — no staging environment, no delivery logs you can read, no test you trust — then the migration isn't one evening, because building the verification is part of the job. I had all three of those in my favor: no walled-garden features in use, a staging site with weeks of runway, and a harness that could prove delivery. That's what "cheap" actually means here — not small, but provable.
The migration: three swaps and three DNS records
Here's the full scope of what one evening actually contained, in order.
Domain verification first, because everything else depends on it. Email providers prove you own your sending domain through DNS: a DKIM record (a public key that lets receiving servers verify your messages were really signed by your domain) and an SPF record (a list of servers allowed to send on your domain's behalf). Resend's version of this is three records: a DKIM TXT record, plus an MX and SPF TXT pair on a send return-path subdomain. I added them at my registrar, watched propagation, and verification flipped green in about five minutes.
One habit worth stealing here: verify DNS with dig, never with a dashboard badge. Some time before all this, a DNS migration had silently dropped SendGrid's authentication records — and every contact email quietly landed in junk folders for who knows how long, while SendGrid's dashboard kept showing "Verified." Those badges are cached snapshots that only refresh when you explicitly click re-verify; mine was months stale. dig TXT resend._domainkey.yourdomain.com answers from reality, before and after any registrar change.
Swap one: the contact route. @sendgrid/mail out, the resend SDK in. Same guardrails around it (rate limiting, validation, bot protection), same behavior — a different courier at the end. While I was in there I renamed the route from /api/sendgrid to /api/contact, because a route named after a vendor is a tiny lie waiting to happen. Vendor-neutral naming means the next provider change touches zero URLs.
Swap two: the capture webhook. The PUT to SendGrid's walled marketing API became a contact-create call against Resend's included contacts:
const resend = new Resend(process.env.RESEND_API_KEY)
const { error } = await resend.contacts.create({
email,
firstName,
lastName,
segments: [{ id: process.env.RESEND_CONTACT_SEGMENT_ID }],
})
The paywalled feature that triggered this whole evening took roughly a dozen lines to have, included, on the new vendor.
Swap three: the CMS adapter. Payload's email transport is pluggable, and there's a first-party @payloadcms/email-resend adapter published in version lockstep with Payload itself. The nodemailer-over-SMTP configuration collapsed into a few lines handing Resend's HTTP API an API key and a from-address. Swapping it was configuration, not archaeology — which is exactly what a first-party adapter is for.
Then verification, because a migration you haven't watched work is a migration you've assumed works: a live form submission through the real UI, the delivery confirmed in Resend's sent-mail log with a delivered status, the message landing in my inbox — not junk, thanks to that fresh DKIM alignment — and a staging sign-up flowing through the webhook into the contact segment within the same second.
What I deliberately didn't do that night
Migrations fail at the edges, so it's worth naming what stayed untouched. I left SendGrid's DNS records in place — they cost nothing to keep, and yanking them the same night I cut over would have meant no working fallback if Resend surprised me. I didn't cancel the SendGrid subscription either; that happens after the new path has a quiet week of real traffic behind it. And I didn't migrate my auth provider's emails — sign-in codes still come from the auth platform's own sender, which is a feature, not a gap: knowing exactly which system owns which category of mail is what makes the next incident debuggable.
Decommissioning is a follow-up errand, not part of the cutover. Do the swap, soak it, then dismantle the old path. The evening you migrate is the worst possible evening to also burn the bridge.
What it cost, what it bought
The whole thing — DNS, three swaps, docs, and live verification — fit in one evening alongside other work. What it bought:
- The bill: $19.95/month of pure delivery becomes $20/month of Pro covering 50,000 emails, ten domains, and the contact storage that started all this. Net cost roughly zero; net capability several times over. (With a single domain, Resend's free tier — 3,000 emails a month — would have made the math even more lopsided.)
- One vendor, three jobs: contact form, CMS transactional mail, and subscriber capture all ride one verified domain now, one dashboard, one delivery log to check when something looks off.
- A consent-shaped capture system: with contacts finally storable, I added an unchecked-by-default "keep me posted" opt-in to the contact form. Nobody joins a list by writing me a message — the checkbox is the consent, and the server captures only when it's ticked and the message actually delivered.
The takeaway
The $240 a year was never really the point. The point is that "it already works" is a reason to keep a vendor exactly until the day it isn't — and that day usually announces itself as a paywall, not an outage. When it does, don't argue with the wall. Inventory the touchpoints, score the four signals, and check whether you're standing in a cheap migration window that's quietly closing.
Your next action: grep your codebase for your most-taken-for-granted vendor's name, count the touchpoints, and put a number on what the migration would actually take. Mine was three touchpoints and one evening. Yours is probably smaller than the inertia has been telling you.