DEV Community

Daniel Dong
Daniel Dong

Posted on

First I killed the signup wall. Then the bots came.

First I killed the signup wall. Then the bots came.

So I killed them too.

Part 1: The wall

Three weeks ago I removed the signup wall from my API product. Instead
of email → password → verification code → API key, visitors landed on
a free playground. 15 models. 10 requests/day. No login. No email.
No friction.

It worked. Registrations jumped from 1 per week to 14. 86% of new
signups were real Gmail accounts. Conversion from playground to
registration hit 19%.

Then something else showed up.

Part 2: The bots

I checked the server logs and found /auth/send-verification-code
getting hammered. 2,600 requests per day. Every single day.

Jul 17: 2,634
Jul 18: 2,713
Jul 19: 2,642
Jul 20: 2,550
Jul 21: 2,745
Enter fullscreen mode Exit fullscreen mode

That's 47-60% of all server traffic. The bots were hitting the
verification endpoint with disposable emails, rotating IPs, never
actually completing registration. Just burning CPU cycles and
wasting bandwidth.

At first I thought "who cares, they're not getting through." But
2,600 requests/day isn't free. On a 1-core VPS, that's real load.
And it makes real traffic analysis impossible when half your data
is noise.

Part 3: The fix (5 lines)

# Before: no rate limit
@app.post("/auth/send-verification-code")
async def send_verification_code(request: Request):
    email = body.get("email")
    # generate code, send email...

# After: one check
@app.post("/auth/send-verification-code")
async def send_verification_code(request: Request):
    client_ip = request.client.host
    now = time.time()

    with _verify_code_rate_lock:
        last_req = _verify_code_rate_limits.get(client_ip, 0)
        if now - last_req < 60:
            raise HTTPException(429, "Too many requests")
        _verify_code_rate_limits[client_ip] = now

    email = body.get("email")
    # generate code, send email...
Enter fullscreen mode Exit fullscreen mode

That's it. A dictionary. A lock. A timestamp check. 1 request per
minute per IP. No Redis. No external rate limiter. No database.
Five lines of in-memory Python.

I also added a domain blocklist. Ten seconds of grep showed the top
abusers: disposable email domains like mailinator, guerrillamail,
and a few others. Blocked at registration.

Part 4: The result

Week Verification requests/day Server load
Jul 17-23 (before) ~2,600 47-60% bot traffic
Jul 24-31 (rate limit deployed) ~2,500 bot slowed but IPs rotated
Aug 1-4 (rate limit + domain block) ~1,600 → 0 decay then silence
Aug 5-9 (now) 0-2 bot traffic eliminated

From 17,000 bot requests per week to zero. Five lines of code. One
domain blocklist.

The pattern

This is the second time I've learned the same lesson: you don't need
a complex solution for a simple problem.

Removing the signup wall wasn't a multi-month redesign. It was one
static HTML page with an 80-line FastAPI endpoint that bypassed auth.

Killing the bots wasn't a rate-limiting service or a CAPTCHA
integration. It was five lines of in-memory rate limiting and a set
of blocked domains.

Both times the fix was simpler than the problem. Both times the
impact was immediate and measurable.

The most effective infrastructure changes I've made to this product
weren't the big features. They were the small, targeted interventions
that removed friction for real users and added friction for bad ones.

AIBridge — 15 Chinese AI models behind one OpenAI-compatible endpoint.
Free playground with no signup. GitHub OAuth login in one click. No
bots wasting your bandwidth (or ours).

aibridge-api.com/playground.html (15 models, no signup)
aibridge-api.com/prompts.html (24 ready-to-use prompts)

1

2

3

Top comments (0)