Should you block disposable emails or just flag them?
· 6 min read · Disposable email
Flag disposable emails by default and block them only where a throwaway address directly costs you money or lets someone repeat a one-time benefit: free trial credits, promo codes, referral bonuses, free API quota. Everywhere else, accept the signup, tag it, and restrict the expensive actions instead of the account. Blocking is cheap to build, but its false positives are invisible, and that's the part most teams get wrong.
This post is about the policy decision. If you still need the detection side, start with how to detect disposable email addresses at signup.
Why "just block them" is a weak default
Blocking feels decisive. Three things make it worse than it looks.
Lists have false positives, and you won't see them. Every disposable domain list includes some domains that shouldn't be there. The maintainers of the most widely used open list say so in their own README: "We cannot guarantee all of these can still be considered disposable." Two popular open lists we compared disagree on thousands of domains, and one of them includes DuckDuckGo's forwarding domain (details in our comparison of disposable email checker options). A user who gets wrongly rejected rarely writes to support. They close the tab. Your dashboard shows a slightly lower conversion rate and nothing else.
The people you stop are mostly not the people you're worried about. A determined abuser creates a fresh Gmail account, registers a domain the lists don't know about yet, or uses a relay. The person most reliably stopped by a blocklist is the one who wanted to try your product before handing over a real address. Some of them would have converted.
Blocking at signup doesn't cover the other door. If users can change their email after signup, a block at registration only applies to people who don't bother to change it later. Whatever rule you pick has to run on email change too.
None of this means blocking is wrong. It means blocking should be a deliberate choice for specific surfaces, not a global setting.
Decide by what the signup gives access to
Detection tells you an address is disposable. The policy question is what a fake account costs you on a given surface. Map each entry point in your product:
| Surface | What a throwaway account costs you | Recommendation |
|---|---|---|
| Free trial with real marginal cost (LLM tokens, SMS, GPU minutes, email sends) | Direct spend, repeated per account | Block, or require a card or phone before the costly part |
| Promo code or referral credit | Money, plus the referrer's reward | Block at redemption, not at signup |
| Freemium product with near-zero marginal cost | Polluted metrics, some support load | Allow and flag; exclude from activation metrics |
| Newsletter or gated content | A lead that will never answer | Allow; mark the contact so sales doesn't chase it |
| Community, comments, reviews | Spam and sockpuppets | Allow and flag; hold first posts for review |
| B2B demo or contact-sales form | Wasted sales time | Allow and flag; route to nurture, not a rep |
Two patterns fall out of this table. First, the strict cases are all about consumable value, so the check belongs at the moment value is handed out, not only at account creation. Second, for most surfaces the harm of a disposable address is noise, and noise is handled with labels and routing, not rejection. Trial abuse has its own playbook in stopping free-trial abuse without punishing real users, and the redemption-time checks are covered in referral and promo-code abuse signals.
What "flag" should actually do
A flag that nothing reads is just a log line. Pick at least one consequence:
- Label the account (for example
signup_risk = 'disposable') and exclude labelled accounts from activation, conversion and retention metrics. This alone makes your funnel numbers more honest. - Gate the expensive action, not the account. Let the user explore, but require a permanent address before issuing an API key, starting trial credits, sending team invites or redeeming a code. The prompt can be friendly: "Add a permanent email to start using your credits."
- Lower the limits. Smaller quota or rate limits until the user adds a permanent address or a payment method.
- Route, don't reject. In a CRM, send flagged leads to an automated sequence instead of a human.
- Queue for review where volume is low and a human decision is cheap, such as B2B onboarding.
Run in shadow mode first
Before you enforce anything, run detection silently for a week or two. Record what you would have done, and keep letting everyone through. Then look at the flagged group:
- How many flagged accounts went on to do something legitimate (paid, invited a teammate, stayed active)?
- Which domains account for most flags? Check the top ten by hand. If a domain looks like a real provider or a forwarding service, your list is wrong for your audience.
- What share of your trial cost or promo payouts went to flagged accounts?
That gives you a real number to weigh against the conversion you'd lose. Without it, you're guessing in both directions.
If you do block, block well
- Write a specific error. "Temporary email addresses can't be used for trials. Any permanent address works, including Gmail or Outlook." Not "Invalid email".
- Enforce on the server. A client-side check is good for instant feedback, and trivial to skip.
- Re-check on email change and at the moment of redemption.
- Keep an escape hatch. A support address in the error message, plus an allowlist you can update without a deploy.
- Log every block with the domain. A cluster of blocks for one domain from users who otherwise behave normally is how you find false positives.
- Never block forwarding services. Apple Hide My Email, Firefox Relay and similar relays point to real inboxes. See why relay services are not disposable email.
Wiring the policy in code
Here's a per-surface policy using pingutil's fast mode. skip_enrichment: true skips the company lookup and returns the category, email_signals and verdict in milliseconds. Note that verdict.is_spam covers two different things: a disposable domain (reason: 'disposable_domain') and a randomized, machine-generated local part (reason: 'randomized_local_part'). The second is a heuristic. It also flags handles like coolcat123@, so we treat it one step softer than a known throwaway domain.
type Surface = 'trial_credits' | 'promo_redeem' | 'freemium_signup' | 'newsletter' | 'demo_request';
type Action = 'block' | 'allow_limited' | 'allow_flagged' | 'allow';
// What to do with a known disposable domain on each surface.
const DISPOSABLE_POLICY: Record<Surface, Action> = {
trial_credits: 'block',
promo_redeem: 'block',
freemium_signup: 'allow_limited',
newsletter: 'allow_flagged',
demo_request: 'allow_flagged',
};
const SOFTER: Record<Action, Action> = {
block: 'allow_limited',
allow_limited: 'allow_flagged',
allow_flagged: 'allow_flagged',
allow: 'allow',
};
export async function screenEmail(email: string, surface: Surface) {
let data;
try {
const res = await fetch('https://pingutil.com/v1/lookup', {
method: 'POST',
headers: {
authorization: `Bearer ${process.env.PINGUTIL_KEY}`,
'content-type': 'application/json',
},
body: JSON.stringify({ input: email, skip_enrichment: true }),
signal: AbortSignal.timeout(1500),
});
if (!res.ok) return { action: 'allow_flagged' as Action, reason: 'check_unavailable' };
data = await res.json();
} catch {
return { action: 'allow_flagged' as Action, reason: 'check_unavailable' };
}
const { verdict } = data;
if (verdict.reason === 'disposable_domain') {
return { action: DISPOSABLE_POLICY[surface], reason: verdict.reason };
}
if (verdict.reason === 'randomized_local_part') {
return { action: SOFTER[DISPOSABLE_POLICY[surface]], reason: verdict.reason };
}
if (verdict.low_trust) {
// role, system or sales-outreach address: fine to accept, worth routing
return { action: 'allow_flagged' as Action, reason: verdict.reason };
}
return { action: 'allow' as Action, reason: null };
}
Store action and reason on the user record. When you revisit the policy in a month, you'll want to know why each account was treated the way it was. Request and response fields are documented in the API reference.
A policy you can adopt today
- Default to flag. Block only on surfaces that hand out consumable value.
- Run the check at signup, on email change, and at redemption.
- Give flags a consequence: labels, gated actions, lower limits or routing.
- Run in shadow mode before enforcing, and review the top flagged domains by hand.
- When you block, say why, offer a way out, and log the domain.
Questions people ask
- Does email confirmation stop disposable email signups?
- No. Disposable inboxes receive mail, so the user clicks your confirmation link like anyone else. Confirmation proves the address works right now, not that it will still work next week.
- Should I block Apple Hide My Email or Firefox Relay addresses?
- No. Relay services forward to a real, long-lived inbox, so there is a real person behind the address who simply chose not to share their primary one. Treat them as normal addresses.
- Is a Gmail plus address like jane+trial@gmail.com a disposable email?
- No. Plus addressing delivers to the same permanent inbox. It is a duplicate-account problem rather than a disposable one, and the fix is normalizing the address before you check for existing accounts.