A contact database becomes a liability the moment it outgrows its own accuracy. This engagement centered on a one-million-record contact database spread across roughly 250,000 sending domains, due to move onto an email platform that bills by the number of stored active profiles. On that pricing model every dead or duplicate address is paid for every month, whether or not a single message is ever sent to it. The work was to reduce the database to addresses worth keeping, and to do it without handing several thousand dollars to a commercial verification service.
At a glance
- Client: An outbound data operation running a one-million-record contact database across roughly 250,000 sending domains, migrating onto an email platform billed per stored active profile.
- Challenge: Loading unverified addresses onto a per-profile platform would waste budget every month and put sender reputation at risk, yet a one-time commercial verification of the full file carried a list price between 2,000 and 5,000 dollars.
- Approach: A staged, resumable verification pipeline in Python and Postgres covering syntax, DNS, role and disposable detection, catch-all detection, throttled SMTP probing, and a confidence-tier model.
- Result: The deterministic layers cleaned the full database for the cost of server time already owned, avoided the one-time verification spend, and cut recurring per-profile platform cost by roughly 2,000 dollars a month.
The context
I was engaged to prepare the database for migration onto a new sending platform, with about two days to do it. The commercial terms of that platform set the stakes. It charges by stored active profile, so the size of the file is a direct and recurring cost, not a one-time import concern. A database that had been treated as free storage for years was about to become a monthly line item, and accuracy was the only lever that would move that number down.
The problem and constraints
The arithmetic was simple and unforgiving. At a million records, every percentage point of dead weight is ten thousand profiles that would be billed indefinitely. A file assembled from several sources over several years is never clean at that scale, and this one had never been assessed as a whole. Beyond cost, there was reputation. Sending to addresses that no longer exist produces hard bounces, and a high bounce rate on a fresh platform is the fastest way to land in spam folders.
The constraints were equally clear. Keep every record, since classification was the goal and deletion was not permitted. Minimize spend. Run everything on a single existing server. Make each stage resumable, because a job that touches a million addresses will be interrupted, and starting from zero each time was not acceptable.
What was in place before, and why it fell short
Before this work the file had no verification layer at all. The prevailing plan was to load it wholesale and let the sending platform sort out what bounced. An earlier attempt along those lines had already degraded sender reputation, which is what prompted a more disciplined approach.
Commercial verification was the obvious alternative, and it is a legitimate tool. The problem is the price at this volume, and, as the testing later showed, its reliability is not what the vendor pages imply for a file dominated by corporate domains. The published rates in 2026 set the baseline the in-house work had to beat.
| Service | Approximate cost to verify 1,000,000 addresses (2026) |
|---|---|
| Bouncer | about 2,000 dollars |
| ZeroBounce | about 2,750 to 3,500 dollars |
| NeverBounce | about 4,000 to 5,000 dollars |
| MillionVerifier (budget option) | about 549 dollars |
What I built
The system was a staged pipeline in Python backed by Postgres, with each stage writing its results and its progress so a restart resumed where it stopped. The stages ran from cheapest and safest to most expensive and most fragile, so that most of the file was resolved before any network traffic that could draw attention.
Stage one, syntax and structure
A strict check against the address grammar in RFC 5321, plus deduplication. This is pure local computation and removes obviously broken records at no cost.
Stage two, the domain layer
Here the key economy appeared. A million addresses collapsed to roughly 250,000 distinct domains, and the two most useful checks, mail-exchanger lookup and catch-all detection, are properties of the domain, not the address. Resolving each domain once with dnspython and caching the result turned a million-item problem into a quarter-million-item one. Domains with no mail exchanger and no fallback address cannot receive mail, so every address under them was marked dead immediately.
Stage three, role and disposable flags
Addresses such as info, sales, and support were flagged as role accounts, and known disposable domains were flagged as well. Neither was deleted. Both were recorded as lower confidence.
Stage four, catch-all detection
For each domain with a live mail exchanger, the pipeline opened one connection and asked the server about a random, certainly nonexistent address, a twenty-character localpart no one would own. If the server accepted it, the domain accepts everything, and no per-address answer from that domain can be trusted. Those domains were labeled accept-all and their addresses were set aside as unprovable.
Stage five, SMTP mailbox probing
For domains that were neither dead nor accept-all, the pipeline verified individual mailboxes with a conversation that never sends a message. It connected to the mail exchanger on port 25, issued EHLO, then MAIL FROM with a benign sender, then RCPT TO for the target address, and read the numeric reply. A 250 indicated the mailbox was accepted, a 550 that it was rejected. The session then issued RSET and QUIT. Crucially it never issued DATA, so no email was ever transmitted. Work was grouped by domain so a single connection could check many mailboxes, and the whole stage was throttled, because probing at speed from one address invites blocks.
Stage six, confidence tiers
Rather than force a binary valid or invalid verdict that the data could not support, every address was placed on a confidence scale.
- Confirmed valid. The mailbox answered honestly to a probe on a server that answers honestly.
- Source or consent verified. A trusted upstream record or an explicit opt-in stood behind the address. For a subscriber, opt-in is a stronger signal than any probe, and it was treated that way.
- Deliverable domain. The domain had live mail exchangers but sat behind a gateway that could not be probed, so the address was plausible but unproven.
- Low confidence. Catch-all domains and role addresses.
- Remove. Dead domains, disposable domains, and malformed syntax.
What was tricky, and what did not work
The headline finding was uncomfortable and worth stating plainly. Per-mailbox SMTP verification, the technique most verification tools are built on, is largely defeated for a corporate file by modern mail security. Classifying the domains by their mail provider showed why.
| Mail provider of the domain | Share of addresses | Honest SMTP answer? |
|---|---|---|
| Microsoft 365 | about 38 percent | No. Rejects probes regardless of whether the mailbox exists |
| Google Workspace | about 24 percent | Rarely. Aggressive blocking of probing addresses |
| Proofpoint | about 15 percent | No. Accepts everything, indistinguishable from a real mailbox |
| Mimecast | about 9 percent | No |
| Self-hosted and other | about 12 percent | Yes |
Roughly 88 percent of the addresses sat behind gateways that either reject every probe, accept every probe, or block the probing address outright. The proof came from a control test. A known-good executive address, confirmed valid through an independent source, was returned as invalid by its Microsoft 365 gateway, purely because the probe arrived from an untrusted address. Any tool that claims reliable per-mailbox validation of Microsoft or Google domains from a single address is overstating what is possible. The honest signal for that majority comes either from services that probe across many rotating addresses, or from consent and source records already in hand.
The practical response was to stop pretending the probe could answer for everyone. Probing was confined to the 12 percent of domains that answer honestly, where it is both accurate and free. For the rest, the pipeline leaned on the domain being alive and on existing consent, and it labeled the uncertainty honestly rather than deleting real people on a false negative.
Two operational notes for anyone attempting the same. Outbound port 25 must be open, and many networks block it, so that is the first thing to test. And the probing address will be blocked or slowed by some servers no matter how politely it behaves, so throttling and patience are not optional.
The outcome
The deterministic layers, syntax, deduplication, dead-domain detection, disposable and role flagging, catch-all detection, and provider classification, ran across the entire million-record file for the cost of server time that was already being paid for. They removed provably dead records at once and sorted the remainder into tiers that a human could act on with confidence. That work alone displaced a one-time commercial verification bill in the low thousands of dollars.
The larger and recurring saving came downstream. Because the destination platform bills per stored active profile, keeping only the addresses worth keeping reduced the monthly cost by roughly 2,000 dollars, and it will keep reducing it for as long as the file is hosted there. The self-hosted checks did not replace commercial verification in every case. They did something more useful. They did the reliable work for free, exposed the part of the problem that no single-address tool can solve, and reserved any future paid verification for the small, ambiguous slice where it would actually earn its price.