Skip to content

How to detect disposable email addresses at signup

· Updated · 7 min read · Disposable email

To detect a disposable email address, reduce the email's domain to its registrable domain, check it against a blocklist that updates at least weekly, and then look at the domain's MX records to catch unlisted domains that share mail servers with known disposable services. Run the check on the server, before you create the account or send a verification email. A blocklist alone gets you most of the way; the DNS step catches some of what the list hasn't seen yet.

This article walks through each layer, what it catches, where it fails, and how to wire it into a signup flow. If you just want framework code, jump to the Node.js and Express guide or the Python, Django and FastAPI guide.

The four layers at a glance

Layer What it catches False positives Cost per check
1. Normalize the domain Subdomain and casing tricks (X@Mail.Yopmail.FR) None by itself Microseconds
2. Blocklist match Established services that have been reported to the list Occasional stale or wrong entries Microseconds
3. MX fingerprinting Unlisted domains that run on a known service's mail servers Low, if you only match hosts on listed domains One DNS query
4. DNS existence signals Dead or half-configured domains Some small real businesses One to three DNS queries

Layers 1 and 2 are mandatory. Layer 3 is cheap and worth adding. Layer 4 is a supporting signal, not a block reason on its own.

Layer 1: normalize before you compare

Every check downstream depends on comparing the right string. Before you look anything up:

  1. Split on the last @. Everything after it is the domain.
  2. Lowercase it and strip a trailing dot.
  3. Convert internationalized domains to punycode, so the Unicode and ASCII forms of a domain match the same list entry.
  4. Reduce it to the registrable domain using the Public Suffix List, so mail.yopmail.fr becomes yopmail.fr and foo.co.uk stays foo.co.uk.

Step 4 is the one people skip. If a service accepts mail on subdomains, a list of exact hostnames is trivially bypassed, which is why the list maintainers store registrable domains only and expect you to match xxx.yyy.zzz against yyy.zzz where zzz is a public suffix. The reasons and edge cases are covered in eTLD+1 explained. In JavaScript, getDomain() from the tldts package does this; in Python, tldextract does.

Layer 2: match against a list that keeps moving

The open dataset everyone starts from is disposable-email-domains. In October 2026 it held about 9,900 registrable domains, up from roughly 4,700 a year earlier, with commits landing most days. It is CC0, so you can ship it inside a commercial product.

What matters operationally is freshness. A copy you checked into your repo is decaying from the day you commit it, because new disposable domains appear faster than any static file can track. Load the list at startup from a source you refresh on a schedule, and keep a small local allowlist for entries you disagree with. The mechanics of keeping it current, and the poisoning incident that shows why you should also review what you pull in, are in why disposable email blocklists go stale.

Also resist the temptation to merge in every list you can find. Bigger lists catch more, and they also block more real people. One widely installed npm package bundles a list of about 121,000 domains and has not had a release since September 2022. That is a lot of entries nobody is reviewing.

Layer 3: fingerprint the mail servers

Here is the observation that makes this layer work: disposable services run many domains on a small number of mail servers. Look at the MX records:

sharklasers.com   MX  mail.guerrillamail.com
guerrillamail.com MX  mail.guerrillamail.com
yopmail.fr        MX  smtp.yopmail.com

When an operator registers a fresh domain and points it at the same mail server, the domain is new but the MX host isn't. So the rule is: if the domain's MX host belongs to a domain that is itself on the blocklist, treat the signup domain as disposable.

How often does that pattern show up? On 12 October 2026 we resolved MX records for a sample of 395 domains from the list. For 66 of them, about one in six, the mail server sat on a different domain that was also on the list. That is the population this layer can catch before a list update does.

Notice what the rule does not do. It does not flag a domain just because its MX host is shared. temp-mail.org uses Cloudflare's email routing hosts, and so do thousands of legitimate companies. Google Workspace, Microsoft 365, Zoho and Cloudflare will always be the most common MX hosts in any dataset, disposable or not. Only match MX hosts whose owning domain is on your blocklist.

Here is a runnable version using DNS-over-HTTPS, so it works in Node 18+, Deno, Bun or a serverless runtime without a DNS library. It uses Cloudflare's JSON DNS API.

// detect.mjs  (npm install tldts)
import { getDomain } from 'tldts';

const LIST_URL =
  'https://raw.githubusercontent.com/disposable-email-domains/disposable-email-domains/main/disposable_email_blocklist.conf';

const blocklist = new Set(
  (await (await fetch(LIST_URL)).text())
    .split('\n')
    .map((line) => line.trim())
    .filter(Boolean),
);

async function mxHosts(domain) {
  const url = `https://cloudflare-dns.com/dns-query?name=${encodeURIComponent(domain)}&type=MX`;
  const res = await fetch(url, {
    headers: { accept: 'application/dns-json' },
    signal: AbortSignal.timeout(1500),
  });
  if (!res.ok) return [];
  const json = await res.json();
  return (json.Answer ?? [])
    .filter((a) => a.type === 15) // MX
    .map((a) => a.data.split(' ')[1].replace(/\.$/, '').toLowerCase());
}

export async function checkDomain(rawDomain) {
  const domain = getDomain(rawDomain.trim().toLowerCase());
  if (!domain) return { disposable: false, reason: 'invalid_domain' };

  if (blocklist.has(domain)) return { disposable: true, reason: 'listed' };

  try {
    for (const host of await mxHosts(domain)) {
      const owner = getDomain(host);
      if (owner && owner !== domain && blocklist.has(owner)) {
        return { disposable: true, reason: `mx_on_listed_domain:${owner}` };
      }
    }
  } catch {
    // DNS timeout: fall through. Never block a signup because your resolver was slow.
  }
  return { disposable: false, reason: null };
}

console.log(await checkDomain('mail.yopmail.fr')); // { disposable: true, reason: 'listed' }
console.log(await checkDomain('stripe.com'));      // { disposable: false, reason: null }

Two practical notes. Cache MX results per registrable domain for a few days; the answer rarely changes and you don't want a DNS round trip on every signup from gmail.com. And short-circuit the common free providers before the DNS call, since they are never disposable. Reading MX records correctly has its own edge cases (the A-record fallback, null MX), so don't treat an empty answer as proof of anything on its own.

Layer 4: DNS existence as a supporting signal

Some signups use domains that are not disposable in the usual sense, just odd: a domain with no MX and no A record, or one that returns NXDOMAIN outright. NXDOMAIN is safe to reject; nobody can receive your verification email there. "Has mail but no website" is weaker. Plenty of real small businesses have a mail-only domain. Use it to add friction (a captcha, manual review), not to block.

Where the check belongs in your signup flow

Placement matters as much as the check itself.

  1. Server side, before creating the user. Client-side checks are for instant feedback only.
  2. Before sending the verification email. Otherwise you are paying to send mail to an inbox that anyone on the internet can read.
  3. On email change, too. A common bypass is to sign up with a real address and switch to a throwaway one later, or the reverse.
  4. On SSO and magic-link paths. If your auth provider creates users on first login, put the check in its pre-registration hook, not just in your own form handler.
  5. Fail open on infrastructure errors. If the list failed to load or DNS timed out, let the signup through and log it. A broken dependency should not take your signup form down.

Disposable domains are only one source of throwaway accounts. Machine-generated local parts on otherwise normal domains (ccjxx90sze@...) are a separate problem with separate detection, covered in detecting machine-generated email addresses.

Doing it with one API call

If you'd rather not maintain the list sync, PSL handling and DNS caching yourself, pingutil runs layers 1 and 2 plus a randomized-local-part check in one request. Fast mode (skip_enrichment: true) skips company enrichment and returns in milliseconds; check_dns: true adds domain_signals for layer 4.

curl -s -X POST https://pingutil.com/v1/lookup \
  -H "Authorization: Bearer $PINGUTIL_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "input": "jane@mail.yopmail.fr", "skip_enrichment": true, "check_dns": true }'

Block on verdict.is_spam (true here, reason: "disposable_domain"). domain_signals is informational and never changes the verdict, so you decide what domain_exists: false means for you. Note what it doesn't do: there is no MX-fingerprinting layer like the one above, so if you want that, keep the snippet. Request and response fields are in the API reference.

Checklist

  • Domain is lowercased, punycoded and reduced to its registrable domain before any lookup.
  • Blocklist is loaded from a source refreshed at least weekly, with a local allowlist override.
  • MX hosts are matched only against domains on your blocklist, never against shared providers.
  • DNS results are cached per domain, and DNS failures fail open.
  • The check runs server side, before verification email, on email change and in SSO pre-registration hooks.

Questions people ask

Can a regex detect disposable email addresses?
No. Disposable domains look like any other domain, and keyword patterns such as 'temp' or 'trash' both miss most services and block real companies. You need a domain list plus DNS checks.
Should I check for disposable email on the client or the server?
On the server. Client-side checks are fine for instant feedback, but anyone scripting signups will call your API directly and skip them.
Does sending a verification email stop disposable addresses?
No. Disposable inboxes receive mail perfectly well, so the user can click the link. Verification proves the inbox exists right now, not that it will exist next week.