---
title: "Block disposable emails at signup"
description: "Throwaway inboxes are how spam, trial-abuse, and fake accounts get in. Checking the email domain against a maintained disposable-domains list at signup takes minutes and pairs perfectly with Cloudflare Turnstile."
date: "2026-05-29"
tags: ["security", "signups", "spam", "frontend"]
---

If you let anyone sign up with any email, a chunk of your "users" will be
throwaway inboxes — `mailinator.com`, `10minutemail.com`, and hundreds more.
They're how people farm free trials, dodge bans, and inflate your numbers with
accounts that will never convert. [@venelinkochev](https://x.com/venelinkochev/status/2042573164245336451)
put it well: blocking these at signup, plus Cloudflare Turnstile for the bots,
is a big lift in signup quality for very little work.

The trick is almost embarrassingly simple: there's a community-maintained list
of disposable domains, and you just check the signup email's domain against it.

## How it works

The [`disposable/disposable-email-domains`](https://github.com/disposable/disposable-email-domains)
repo publishes a `domains.json` — tens of thousands of known throwaway domains,
updated continuously. Fetch it (cache it for a day), drop it in a `Set`, and
membership-test the domain after the `@`:

```ts
const LIST =
  "https://rawcdn.githack.com/disposable/disposable-email-domains/master/domains.json";

let cache: Set<string> | null = null;
let fetchedAt = 0;

async function disposableDomains() {
  if (cache && Date.now() - fetchedAt < 86_400_000) return cache; // 1 day
  const res = await fetch(LIST);
  cache = new Set<string>(await res.json());
  fetchedAt = Date.now();
  return cache;
}

export async function isDisposableEmail(email: string) {
  const domain = email.split("@")[1]?.toLowerCase().trim();
  if (!domain) return false;
  return (await disposableDomains()).has(domain);
}
```

Then guard the signup handler:

```ts
if (await isDisposableEmail(email)) {
  return new Response("Please use a permanent email address.", { status: 422 });
}
```

A `Set` lookup is O(1), and the daily-cached fetch means you're not hammering
the CDN. On an edge runtime like Workers, keep the cache in module scope (or KV)
so it survives between requests.

## Do it server-side

The check belongs on the **server**, at the signup endpoint — never trust a
client-only check, since anyone can skip it. The interactive demo above runs in
your browser only because it's a demo; in production this is a few lines in your
API route.

## Layer it

No single signal is enough on its own. Stack cheap ones:

- **Turnstile** (or hCaptcha) to filter out bots before they submit.
- **Disposable-domain block** to filter out throwaway humans.
- **Verification email** so a real, owned inbox is still required.

Each is minutes of work; together they cut a surprising amount of junk.

## Takeaways

- Disposable inboxes are a top source of fake/abuse signups — block the domain
  at signup against a **maintained list**, not a hand-rolled regex.
- Fetch `domains.json` once a day into a `Set`; the check is O(1) and runs
  server-side.
- Combine with **Turnstile** and email verification for layered, low-effort
  defense.

I put a live checker and the copy-paste guard on the [Freebies](/freebies) page.
