Errata: The PTR Record, I Was Wrong (Thank You, Yahoo)
About that time I confidently understated the importance of PTR records, got soft-rejected into oblivion by Yahoo's postmaster, and relearned that email deliverability is an endless war against capitalism.
In my previous mailcow post, I addressed the PTR record with the breezy confidence of someone who had been lucky enough not to encounter strict enforcement yet. My position was roughly: “I haven’t run into many that are strict about it when the other controls all check out. Your mileage will vary.”
It did not, in fact, pay off.
It came back to visit a few days ago, when a verification email for an app invite to a friend failed to send. What I hoped would be a quick “does this work?” test turned into a three-hour rabbit hole of Postfix retries, a crash course in deliverability fundamentals, two VPS bills - the first for a server I could never use, having enthusiastically paid for it before checking whether the provider allows outbound port 25 - and several days writing this, a formal apology to my own stupidity.
What happened
I sent mail to a Yahoo address. Yahoo’s MTA - Mail Transfer Agent, the server on the receiving end whose job is to accept or refuse your mail - returned:
450 4.7.25 Forward-confirmed reverse DNS failed tnmpmscs (in reply to MAIL FROM command)
Not once. Repeatedly, across three different Yahoo MTAs (mta5.am0.yahoodns.net, mta6.am0.yahoodns.net, mta7.am0.yahoodns.net), all reaching the same conclusion. Here’s what the log actually says, for posterity and shame:
2026-07-02, 05:23:53 pm info B05F2B22244: to=<[email protected]>,
relay=mta7.am0.yahoodns.net[67.195.228.111]:25, delay=8068,
status=deferred (host mta7.am0.yahoodns.net[67.195.228.111] said:
450 4.7.25 Forward-confirmed reverse DNS failed tnmpmscs
in reply to MAIL FROM command)
delay=8068 is that message sitting in the queue for two hours and fifteen minutes, being handed to Yahoo over and over, and getting the same answer every time. And Then:
Two hours and fifteen minutes is also how long it took before my friend asked me why they hadn’t got an email from me. That’s worth sitting with. My monitoring didn’t tell me. Nothing bounced. No alert fired. The thing that finally surfaced a mail server fault on infrastructure I built, provisioned, and wrote a very long blog post about was a human being wondering where their email went.
And as far as I can tell, they never did get it. I have no delivery confirmation, no bounce, and no acknowledgement from Yahoo that the message ever landed - just a queue entry that kept failing and a friend who eventually stopped waiting. I can’t prove it wasn’t delivered on some later retry, which is precisely the problem: I have no evidence either way. That’s the actual state of things after three hours of investigation. Not “it failed,” not “it worked,” just an absence of any signal at all in either direction.
It’s a soft reject, and that matters
One correction to make before I go any further, because I originally wrote this whole thing up as Yahoo “rejecting” my mail. It didn’t. 450 is a 4xx response, which in SMTP means temporary - “not right now, try again later.” A hard rejection would be a 5xx. What Yahoo actually did was soft-reject, or defer, every single attempt.
That distinction is not pedantry, it’s the entire diagnostic experience:
- A
5xxtells you immediately. The mail bounces, you get a failure notice, you go read the error. - A
4xxtells you nothing, slowly. Postfix does exactly what it’s supposed to and keeps retrying with backoff. Nothing bounces. The sender sees no error at all. The mail just… doesn’t arrive, and won’t, untilmaximal_queue_lifetimeruns out - five days by default - at which point it finally gives up and bounces.
So the failure mode isn’t “Yahoo told me my PTR was broken.” It’s “my mail silently didn’t turn up, and the only place anyone was being told why was a log file I had to go and look at.” The answer was sitting in the queue log the whole time, phrased perfectly clearly, waiting for me to bother reading it.
Which means the deferral didn’t just delay the mail, it delayed the diagnosis, outsourced the alerting to the recipient, and left me unable to say for certain what happened to a message I sent from my own server. If that had been a password reset instead of an app invite, the person waiting on it would have assumed my app was broken, and they’d have been right, just not about which part. And I’d have had nothing to tell them beyond “I think it’s in a queue somewhere.”
A note on the tnmpmscs string
If you look closely at the error there’s a string tacked onto the end: tnmpmscs. My first instinct was that this was my own server’s HELO greeting being read back to me, and I wasted a genuinely embarrassing amount of time on that theory. It isn’t. It comes from Yahoo’s side.
The tell is that other people get the exact same string on completely unrelated failures. It turns up in the wild attached to 552 message size exceeds maximum size, to User is receiving mail too quickly, to Max message per connection reached, closing transmission channel - different senders, different servers, different checks, same eight letters bolted onto the end. Nothing about a size limit or a rate limit has anything to do with my hostname, so tnmpmscs clearly isn’t describing me.
That also rules out the slightly more sophisticated theory I briefly held, which was that it identifies which policy check inside Yahoo’s system did the rejecting. It can’t. If it were a per-check code it would differ between “your message is too big” and “your reverse DNS doesn’t resolve,” and it doesn’t. It’s the same string across all of them. Whatever it is - a host tag, a stack identifier, something an SRE at Yahoo would recognise and nobody outside would - it’s invariant, and Yahoo doesn’t publish a decoder for it. It’s the kind of thing you’d quote verbatim if you ever opened a case with their deliverability team, and otherwise safely ignore.
So the practical advice is: the arbitrary-looking string is not the message. The message is everything before it - Forward-confirmed reverse DNS failed - and that’s the part you act on. Don’t do what I did and treat the mystery token as the interesting half of the error. It’s the least informative thing in the entire line.
Ruling out my HELO properly
Rather than keep theorising about what my server was announcing, I went and asked it. You can do this to your own mail server in about ten seconds:
220-mail.thefathacker.tech ESMTP Postcow
HELO test.thefathacker.tech
521 5.7.0 Error: I can break rules, too. Goodbye.
There’s the answer in the first line: my server greets as mail.thefathacker.tech. Not a short hostname, not whatever MAAS generated, the actual FQDN. Which makes sense once you think about where it comes from - mailcow runs Postfix in a container and drives myhostname from MAILCOW_HOSTNAME in mailcow.conf, so the host OS’s hostname never gets a vote in what reaches SMTP at all. My HELO was fine the whole time. (Postcow is mailcow’s own joke in the banner, and I enjoy it every time.)
The 521 is my own server telling me off, and it’s my favourite part. Postfix noticed I typed HELO while it was still mid-way through sending a multi-line 220- greeting, decided that was a pipelining violation, and hung up with “I can break rules, too. Goodbye.” That’s reject_unauth_pipelining working exactly as intended - real spambots blurt commands without waiting, so not waiting is a tell. I got caught by my own spam defences while investigating my own mail server. Two wrong theories in one section, both put down by simply looking instead of reasoning about it from the armchair.
What I got wrong
I was wrong about two things:
1. How seriously some providers actually take PTR. It’s not a “pickiest of the big providers” edge case. Yahoo is a big provider, running mail at a scale I will never come close to - millions of mailboxes and the volume that goes with them. I send a handful of app notifications a day. That asymmetry is precisely the point: they set the terms, I meet them, and they will defer my mail into a five-day queue until it dies, solely because my PTR doesn’t validate. I treated this like a soft recommendation from a standards document. Yahoo treated it like a gate.
2. How to explain the gap at all. I framed the missing PTR as “a gap in my current setup,” something I was “owning up to” in a confession, and then immediately deflected with “in practice though, I haven’t run into many that are strict.” That was cowardly. It was also wrong. There’s a difference between being unlucky and being wrong. I was wrong.
What Yahoo actually says
Yahoo’s current Sender Requirements & Recommendations lists “a valid forward and reverse DNS record for your sending IPs” as a requirement. Not a recommendation, and not a bulk-sender-only concern - it appears under the requirements for all senders as well as the bulk ones. It took me getting deferred for two hours to accept that they meant it.
The fix
The simple fix is to set a PTR record. Ask whoever controls the IP block and they’ll set it. Done. That is the entire correct solution, a one-line fix sitting there waiting to be made.
Checking whether you even have one
Before anything else, go and look. dig -x on your sending IP, and be honest about what comes back. Here’s the IP my mailcow was actually sending from:
root@pmg-sr-1:~# dig -x 152.44.254.123
; <<>> DiG 9.20.26-1~deb13u1-Debian <<>> -x 152.44.254.123
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: 46496
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1
SERVFAIL, with an empty answer section. Not even a polite NXDOMAIN - the reverse zone for that block isn’t answering at all. Whichever way you read it, there is no PTR, and that is the whole story of this post in six lines of output.
Now the relay I built to get around it:
root@pmg-sr-1:~# dig -x 5.78.82.169
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 44579
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; ANSWER SECTION:
169.82.78.5.in-addr.arpa. 86400 IN PTR pmg-sr-1.hil1.thefathacker.tech.
NOERROR, one answer, a real hostname. That’s half of what you need. The other half is the forward confirmation - the hostname that PTR handed you has to resolve back to the same IP, or you’ve built a one-way street and FCrDNS still fails:
dig +short pmg-sr-1.hil1.thefathacker.tech
If that returns the IP you started with, the round trip closes and you’re done. Two commands. I could have run them in June.
When there’s nobody to ask
“Ask whoever controls the IP block” quietly assumes there’s somebody to ask who’ll say yes. For a lot of people there isn’t, and this is where I want to sharpen something I hedged in the last post. I wrote then that on a residential connection you “may not be able to” set a custom PTR. Having now watched a major provider defer my mail into the void over exactly this, let me put it more plainly: on most residential plans you cannot, and that is a hard blocker, not an inconvenience.
A static IP is not the same as a controllable one. Plenty of residential plans will happily sell you a static address while keeping reverse DNS entirely to themselves - the ISP owns the in-addr.arpa delegation, so the PTR is theirs to set and yours to live with. Business plans from the same ISP often will set one on request. Residential, generally, will not.
And the trap is subtler than “you have no PTR.” Frequently you have one, it’s just theirs: something along the lines of host-203-0-113-45.dsl.example.net. That will forward-confirm perfectly well, so strictly speaking you pass FCrDNS - which is why this isn’t quite the same failure I hit. What you can’t do is make it say mail.yourdomain.com, so your reverse DNS and your HELO will never agree, and a hostname with dsl or dynamic or pool or your own IP dashed into it is a well-known spam signal in its own right. You end up technically compliant and still judged, which is arguably worse than failing outright, because there’s nothing left to fix.
Stack that on the residential problems from the gotchas - port 25 blocked inbound, whole ranges blocklisted by default - and the conclusion is the one I’ve been circling since the first post, now with a receipt attached: the thing you actually need isn’t a static IP, it’s authority over the reverse DNS for it. If you don’t have that and can’t get it, no amount of immaculate SPF, DKIM, and DMARC buys your way past a receiver that has decided to care. Relay through something that does have that authority, or don’t send from there.
My fix (because why make it easy)
My approach was not the simple one. The PTR is controlled by whoever owns the IP block, and my colo provider can’t currently make that change for me. To be fair to them, they do a great job overall - they’re a small operation and self-service reverse DNS isn’t the priority for them that it is for me. Things are improving. For now, it’s the gap.
So instead of setting a PTR record, which is free, I went and paid money for a third server. Third, because the mailcow build already runs to two - the primary and that cold spare I keep promising to test.
Let’s be clear about the trade. A PTR record costs nothing. A VPS does not. This is unambiguously the more expensive option, and I want to be precise about the claim I’m making for it: it isn’t the cheapest path, it’s the cheapest path available if I insist on owning the solution - and I do insist, so here we are. That constraint is entirely self-imposed. Nobody made me refuse the managed option; I just prefer the version where the thing is mine.
Which means I also own everything downstream of that preference: a third machine to patch, a second mail host in the delivery path, another set of certificates to renew, another box that can break at three in the morning, and a brand new single point of failure for every piece of outbound mail I send. None of that is on the invoice. All of it is the actual price. Bad decisions, arguably - but they’re my bad decisions, and buying the managed version would have meant inheriting someone else’s instead, sight unseen. I’d rather hold the whole set and know exactly which ones are mine.
So, I spun up a Proxmox Mail Gateway instance on a Hetzner VPS (pmg-sr-1.hil1.thefathacker.tech) and configured proper reverse DNS on its sending IPs:
| IP | Reverse DNS |
|---|---|
| 5.78.82.169 | pmg-sr-1.hil1.thefathacker.tech |
| 2a01:4ff:1f0:d231::1 | pmg-sr-1.hil1.thefathacker.tech |
And don’t forget the forward part of forward-confirmed reverse DNS, which is the half people skip:
# Zone: thefathacker.tech
pmg-sr-1.hil1 A 5.78.82.169
pmg-sr-1.hil1 AAAA 2a01:4ff:1f0:d231::1
Then I pointed mailcow’s outbound SMTP at it. My own mail server no longer touches port 25 for external delivery; the PMG does. The PMG has a clean IP with proper PTR and FCrDNS, DKIM signing on a dedicated selector (thefathacker-pmg-hil1), TLS, and the whole works. Mailcow becomes the MSA for authenticated submission, and the PMG handles everything internet-facing.
Before any of that, the relay wouldn’t start
Worth recording, because it cost me an evening and the error message is not obviously about what it’s about. PMG’s Postfix refused to start at all:
postfix: fatal: bad string length 0 < 1: mydomain =
Postfix derives mydomain by stripping the first label off myhostname. Hand it a host that can’t work out its own fully-qualified name and mydomain comes out empty - and Postfix, sensibly, would rather die than guess. The fix isn’t in Postfix at all. It’s telling the resolver what the host’s search domain is, and which file you edit depends on the provider’s image:
# Debian on DigitalOcean - systemd-resolved
# /etc/systemd/resolved.conf
[Resolve]
Domains=sfo3.thefathacker.tech
# Debian on Hetzner - resolvconf
# /etc/resolvconf/resolv.conf.d/tail
search hil1.thefathacker.tech
Same distribution, same release, two entirely different files, because “Debian 13” from two providers is not the same install. Notice the theme, though: a mail server that could not determine its own domain name. That’s twice in one post I’ve lost time to a hostname problem, and neither time was the software wrong to complain.
The one piece of configuration you cannot skip is adding the mailcow host to PMG’s trusted networks (Configuration → Mail Proxy → Networks). This isn’t a filtering nicety, it’s what makes the relay function at all. PMG decides whether a message is outbound by asking whether it arrived from a trusted network, and it will not relay for anything else - because the alternative is being an open relay, and the internet has quite enough of those. Get it wrong and PMG refuses your own mail server. Get it right and mail from colocation is treated as outbound, picks up the outbound rule set, and leaves over the Hetzner IP.
Inbound never goes near PMG. The colocation mailcow already runs Rspamd and ClamAV doing the same job PMG would do, so inbound mail takes the direct route on mailcow’s own infrastructure while outbound gets the clean PTR treatment from Hetzner.
To be clear about what this is: proper PTR is still the correct solution. The PMG relay is a workaround that happens to work for me. Same result, different mechanism, considerably more moving parts.
Did it actually work?
I went to mildly absurd lengths to find out: I registered a Yahoo account of my own to test against, on the grounds that “ask my friend whether my mail server is working yet” is not a monitoring strategy and I’d already leaned on that once.
It worked. Yahoo accepted the message - no 450, no deferral, no three-hour queue. The headers on the delivered mail confirm the whole path did what it was built to do: mailcow handed off to the PMG, the PMG delivered to Yahoo over TLS 1.3, and Yahoo recorded the originating IP as the relay’s 5.78.82.169. SPF passed, designating that IP as a permitted sender. DMARC passed, against a p=reject policy. The PTR fix, or rather the PTR workaround, does exactly what I built it for.
And it went straight to the spam folder.
Which is the lesson I keep having to learn in this series: getting accepted and getting delivered are two different achievements, and I had only bought the first one. There’s nothing mysterious about the second. A brand new relay IP with no sending history, two active blocklist entries, and a test message whose entire body was the words “But Does it?” - I would have filed that under spam myself. The PTR record got me through the door. It did not get me a seat.
The bit I wasn’t expecting
Reading those headers properly turned up something I hadn’t gone looking for. There are two DKIM signatures on that message, because mailcow signs it and then the PMG signs it again, and Yahoo’s verdict on them was not the same:
| Signature | Selector | Result |
|---|---|---|
| mailcow’s | dkim |
pass |
| the PMG’s | thefathacker-pmg-hil1 |
perm_fail |
perm_fail is a permanent failure, not a transient lookup problem - Yahoo could not validate that signature and would not be able to next time either. Which is a slightly awkward footnote to describing that selector a few paragraphs ago as part of “the whole works.”
The reason nothing appeared broken is that DMARC only needs one aligned signature to pass, and mailcow’s held up. So my mail authenticated, my reports looked clean, and a signature I had deliberately added was failing on every single message. That is precisely the class of fault this entire post is about: a control that looks configured, reports nothing, and is doing nothing. I only found it because I finally read the headers instead of assuming.
And before that starts to read as diligence: I want to be clear that I wasn’t auditing anything. I had copied those headers out to show off that the PTR fix worked, and the broken signature was sitting in the middle of the evidence I was using to brag. I didn’t go looking for it. I fell over it while admiring my own work.
It gets worse, because I had already written the instructions. The first post has an entire section called DKIM, and why “alignment” is the whole game, in which I explain that your server holds the private key and publishes the matching public key in DNS, and that DMARC is satisfied the moment one aligned check passes. That is the step I skipped, and the mechanism that stopped me noticing, both documented in my own words six weeks earlier. I didn’t fail to know it. I just didn’t do it.
Why it failed, and the part worth stealing
I had published the key. I had published it in the wrong zone, which is a more interesting mistake, because it feels completely correct right up until it doesn’t work.
A DKIM signature carries a d= tag naming the domain making the claim, and a verifier takes that domain and looks for the key at <selector>._domainkey.<that domain>. The relay signs as whichever domain the mail is from, so for a test message sent from fathaxz.wtf, Yahoo went looking for thefathacker-pmg-hil1._domainkey.fathaxz.wtf.
I had put the record in thefathacker.tech - the domain that owns the relay, where the selector was generated, and where it felt like it belonged. That domain appears nowhere in the signature. Nobody looks there. As far as every verifier on the internet was concerned the key did not exist, and they were right, because for the domain that was actually making the claim it didn’t.
This is the bit worth internalising if you run one relay for several domains: the key has to be resolvable under every domain you sign for, not under the machine doing the signing. The signing host’s own domain is irrelevant to the lookup. Ten sending domains means ten places that selector has to resolve.
Which is a horrible thing to maintain by hand, so don’t. Publish the actual key once, in whichever zone you consider home, and point everything else at it with a CNAME:
# in fathaxz.wtf
thefathacker-pmg-hil1._domainkey CNAME thefathacker-pmg-hil1._domainkey.thefathacker.tech
Verifiers follow that happily. One record holds the key, every other domain borrows it, and when the time comes to rotate you edit one TXT record instead of hunting through a dozen zones for the ones you forgot.
There’s a second trap waiting in the same job, which caught me on the way through. PMG hands you the record in BIND’s multi-line form, split across several quoted strings:
thefathacker-pmg-hil1._domainkey IN TXT ( "v=DKIM1; h=sha256; k=rsa; "
"p=MIICIjANBgkqhkiG9w0BAQEFAAOCAg8A..."
"..." )
That’s not decoration. A single DNS character-string caps at 255 bytes, and a 4096-bit RSA key runs to 764, so it physically cannot be one string. Most DNS providers want the whole value in one field and will chunk it for you behind the scenes - but paste that block in verbatim, quotes and tabs included, and you publish a record containing literal " characters that no verifier can parse. Paste it somewhere that truncates rather than chunks and you publish a key that stops halfway through. Either one lands you at the same perm_fail by a different route.
So: concatenate the quoted segments into one continuous value, drop the quotes, and let the provider handle the 255-byte split.
Re-tested afterwards, and this time Yahoo agreed with itself:
| Check | Selector | Result |
|---|---|---|
| DKIM | thefathacker-pmg-hil1 |
pass |
| DKIM | dkim |
pass |
| SPF | - | pass |
| DMARC | p=reject |
pass |
Two signatures, both valid, on a relay with correct reverse DNS. Which is where I should have been in June.
Why not just move mailcow entirely?
Colocation is far cheaper than renting the kind of hardware I already own from a VPS provider. Relaying only outbound through Hetzner is the minimum viable answer to the PTR requirement - a small VPS doing one job - rather than working through reverse DNS changes with my colo provider or rebuilding anything at home.
This is also where that first wasted bill comes in. I originally spun this up on DigitalOcean, paid for it, and then discovered they block port 25 outright (Why is SMTP blocked?) with no exceptions, no appeals process, and no amount of being a nice customer about it. A mail relay that cannot use port 25 is a very expensive way to run nothing at all.
So: check the port 25 policy before you enter a card number. Every provider I looked at blocks it by default. The only thing that varies is what happens next:
| Provider | Getting port 25 opened |
|---|---|
| DigitalOcean | You don’t. No exceptions, no appeal. |
| Vultr | Submit a support ticket with your use case, authentication setup, and expected volume. Reviewed case by case and explicitly not guaranteed. |
| Hetzner | Apply, and they genuinely do approve - but not on a brand new account. Yours needs to be in good standing first. |
That last caveat is the one I’d want to know in advance, because “just use Hetzner” is not advice you can act on the day you sign up. The account has to have some history before the request goes anywhere, which quietly makes “spin up a relay this afternoon” a plan with a waiting period in it.
Future problems, because fixing one spawns two
I’m not in the clear. The Hetzner IP I use for PMG is currently on two blocklists:
| Blocklist | What it actually means |
|---|---|
| RATS NoPtr | Listed for having no reverse DNS - which was entirely true, right up until I set one |
| BARRACUDA | A reputation listing on the IP, most likely inherited from whoever held 5.78.82.169 before me |
I first wrote that up as “previous abuse on those addresses.” That’s half right at best. Barracuda is plausibly someone else’s baggage, and it’s exactly the sort of thing that makes VPS providers so twitchy about port 25 in the first place. RATS NoPtr is not inherited at all - NoPtr lists IPs for precisely the thing its name says, and that IP had no PTR because I hadn’t got round to setting one. I earned that one entirely on my own merits.
The NoPtr listing should clear itself on the next rescan now that the record exists. Barracuda may age out, or I may need to go and read their review process. Either way I’ve started this relay’s life with a mildly tainted reputation, which is a fitting punishment.
Things not yet done
The TODO list is a series tradition:
- Fix the PMG DKIM signature - done. The key was published, just in the relay’s own zone instead of the domain doing the sending. Both signatures verify now.
- CNAME the selector into every other sending domain -
fathaxz.wtfwas the one I happened to test with, and it’s the only one I’ve fixed. Every other domain this relay signs for has exactly the same hole in it, silently, right now. - Alert on the mail queue, not on the mail server - the one this whole post is really about. Everything I monitor said “healthy” while nothing was being delivered. I need an alert on deferred mail sitting in the queue past a sane threshold, because “a friend asks you” does not scale and did not even work quickly.
- Get out of the spam folder - accepted is not delivered. A new relay IP with no sending history and two blocklist entries needs warming and patience, and reputation has no one-line fix.
- Confirm the RATS NoPtr delisting - it should drop off on rescan. “Should” is doing some work there.
- Investigate the Barracuda listing - read their process, decide whether it’s worth submitting or whether it ages out on its own.
- Revisit the colo PTR - the moment my provider can set reverse DNS, the relay becomes optional and the correct fix becomes available. Keep asking.
- Acknowledge the new single point of failure - all outbound now depends on one small VPS. The cold spare is still untested and now has company.
- Check the PMG’s own HELO and banner - greets as
pmg-sr-1.hil1.thefathacker.tech, which is exactly what its PTR says. The one hostname in this entire saga that was right first time. - Reconcile the message size limits - the relay advertises
SIZE 10485760, a hair over 10MB. If mailcow accepts anything larger it will hand over a message the relay then refuses, and the sender gets a bounce for mail their own server already took. Two hosts in a path means two limits that have to agree. - Turn off VRFY on the relay - advertised, reachable, and useful to precisely nobody except someone enumerating my addresses.
Why this is so damn hard
The reality of email is that it is deeply, fundamentally hard, and that difficulty is the entire reason the enterprise email industry exists. If you’re wondering why self-hosting feels like a relentless slog, there’s a talk that explains it better than I can:
Email vs Capitalism, or, Why We Can’t Have Nice Things - Dylan Beattie, NDC Oslo 2023.
This is why M365 exists. This is why Proofpoint exists. This is why SendGrid exists. Deliverability isn’t plumbing you install once, it’s an ongoing fight against abuse, reputation decay, blocklists, and receiving policies that change without telling you. Every provider runs its own reputation scoring. Every IP needs warming. Nobody sends you the changelog.
And the cruellest part is how little of it you get told. I was half lucky. The message I eventually found was excellent - plain English, naming the exact control that failed - but nothing delivered that message to me. It sat in a queue log waiting to be discovered while my friend wondered where their email was. Plenty of senders don’t even get the good half: no detail, or nothing at all while their mail quietly lands in a spam folder. If the big providers will defer your mail to death over a missing PTR - and they hold the keys to a huge share of the world’s inboxes - then PTR isn’t optional. It’s a prerequisite.
You can buy your way out of all of this. Mimecast and Proofpoint will handle deliverability, reputation, and the DNS plumbing for a genuinely professional price. SendGrid and Amazon SES will ship your outbound over clean IP space for a fraction of it - and SendGrid’s free tier, 100 emails a day, would have covered everything I was trying to do here comfortably, for nothing at all. Paid plans start somewhere around $20 a month if you outgrow it. I chose not to use any of them. I’m acutely aware I’ve spent the better part of a week writing a blog post about a problem that a free account would have made vanish. That awareness does not change my preference.
Lesson learned: don’t be lazy
The lesson that applies well beyond email: don’t dismiss a technical control because it’s been convenient for you so far. Your convenience is not evidence that a control is unimportant. It’s only evidence that you haven’t been made to pay the tax yet.
And read the error message - the whole message, not the interesting-looking bit. Mine named the failing control in plain English on the very first attempt: Forward-confirmed reverse DNS failed. I spent hours on the eight characters that came after it, because a mystery is more fun than a chore. The chore was the answer.
Correction
To quote myself from the previous post and correct it directly:
“In practice, though, I haven’t run into many that are strict about it when the other controls all check out.”
Correction: Yahoo is strict about it, and demonstrated as much conclusively. My previous statement was false. If your SPF, DKIM, and DMARC are immaculate but your PTR is missing, mail to Yahoo will be deferred - repeatedly, quietly, and eventually into a bounce five days later. Or, at minimum, you’ll spend an afternoon watching Postfix retry deliveries that were never going to succeed.
TL;DR
I was wrong about PTR records. Yahoo settled the argument. I fixed mine - by which I mean I moved the problem to a different server that has one, which is not quite the same thing as fixing it and I’d rather say so plainly. Mail now reaches Yahoo. It reaches the spam folder, but it reaches it, and that’s a different problem with a longer fix. If you skipped this step because of my earlier post, shit may not be getting delivered, and nothing is going to tell you. Not a bounce, not an alert, not your monitoring. Go and read your headers and your queue.
Spoilers for future content: there’s an app deployment in the works that I’ll be writing about. It triggered all of this. Stay tuned. And keep a fire extinguisher near the rack. Or don’t, i’m not your mum.
Disclaimer, as ever: this post was written with the help of Claude Opus and Qwen3.6-35B-A3B-FP8 (Another future project to discuss), and as ever I’ve read and approved every word - including the ones where I’m correcting myself inside a correction. Both wrong theories about tnmpmscs, the hours spent on the least useful string in the error, and the decision to buy two servers to send one email - one of which I could never use - are regrettably, verifiably, entirely my own. Getting told off by my own pipelining check was a bonus. The cold spare is still untested, and now there are three of them.