Stop Django Signup and Form Spam With Turnstile
Stop bot signups and contact-form spam in Django: add Cloudflare Turnstile with server-side verification, honeypot fields and timing checks, rate limits, email confirmation, and how to keep forms accessible and private.
Problem statement
Bots create thousands of fake accounts, flood contact forms with spam links, and submit newsletter forms with other people's addresses. The fake signups pollute your database and analytics, and the confirmation emails they trigger hurt your sender reputation and can get your domain blocklisted. Traditional CAPTCHAs annoy real users. Cloudflare Turnstile (free, privacy-friendly, often invisible) combined with cheap server-side checks such as honeypots and rate limits stops most automated form abuse without punishing people.
- Create a Turnstile widget in the Cloudflare dashboard (no need to proxy the site through Cloudflare) and get the site key and secret.
- Add the widget script and
<div class="cf-turnstile" data-sitekey="...">to signup and contact forms. - Verify
cf-turnstile-responseserver-side in the form'sclean()by POSTing to the siteverify endpoint with your secret and the user's IP. - Add a hidden honeypot field and a minimum fill time; reject submissions that fill the trap or arrive in under two seconds.
- Rate-limit signup and contact endpoints per IP.
- Require email confirmation before activating accounts or sending anything else to the address.
Step-by-step solution
1. Add the widget
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<div class="cf-turnstile" data-sitekey="{{ TURNSTILE_SITE_KEY }}" data-action="signup"></div>
<button type="submit">Create account</button>
</form>
The widget runs a background challenge and adds a hidden cf-turnstile-response field with a short-lived token. Most visitors never see an interactive challenge.
2. Verify on the server
import requests
from django import forms
from django.conf import settings
def verify_turnstile(token, remote_ip):
if not token:
return False
try:
r = requests.post(
"https://challenges.cloudflare.com/turnstile/v0/siteverify",
data={"secret": settings.TURNSTILE_SECRET, "response": token, "remoteip": remote_ip},
timeout=5,
)
data = r.json()
except (requests.RequestException, ValueError):
return False
return data.get("success") and data.get("action") == "signup"
class SignupForm(forms.Form):
email = forms.EmailField()
password = forms.CharField(widget=forms.PasswordInput)
def __init__(self, *args, request=None, **kwargs):
super().__init__(*args, **kwargs)
self.request = request
def clean(self):
cleaned = super().clean()
token = self.data.get("cf-turnstile-response", "")
if not verify_turnstile(token, self.request.META.get("REMOTE_ADDR")):
raise forms.ValidationError("Please complete the verification and try again.")
return cleaned
Never trust the presence of a token alone; tokens must be verified with the secret, are single-use, and expire after a few minutes. Decide what happens if siteverify is unreachable: failing closed blocks signups during outages, failing open lets bots through briefly; most sites fail closed for signup and log the incident.
3. Honeypot and timing
class SignupForm(forms.Form):
website = forms.CharField(required=False, widget=forms.TextInput(attrs={"autocomplete": "off", "tabindex": "-1"}))
started = forms.FloatField(widget=forms.HiddenInput)
def clean(self):
cleaned = super().clean()
if cleaned.get("website"):
raise forms.ValidationError("Invalid submission.")
if time.time() - (cleaned.get("started") or 0) < 2:
raise forms.ValidationError("Please take a moment and try again.")
...
Hide the honeypot with CSS (not type=hidden, which bots skip) and set started when rendering. Sign the timestamp (Django's signing module) so bots cannot fake it.
4. Rate limits and confirmation
Limit signups and contact submissions per IP at the edge or with django-ratelimit. Create accounts as inactive until the email link is clicked; never send marketing or welcome emails before confirmation. Remove unconfirmed accounts after a few days.
5. Accessibility and privacy
Turnstile is designed to work without visual puzzles and supports screen readers; keep a clear error message and a way to retry. Mention it in your privacy notice, since it processes visitor signals.
Verification check: submit the form normally and succeed; submit with the honeypot filled or within one second and get rejected; post directly with curl and a fake token and get a validation error; check that the daily count of unconfirmed accounts drops.
Explanation
Turnstile runs non-interactive browser challenges and returns a token that only your server, holding the secret, can verify with Cloudflare. Bots that submit forms directly with HTTP clients cannot produce valid tokens. Honeypots and timing checks catch unsophisticated bots at no cost, rate limits cap whatever gets through, and email confirmation ensures accounts belong to someone who controls the address. Together they make abuse expensive while keeping the form easy for people.
Edge cases / notes
- SPAs and APIs should send the token in the request body and verify it in the API view the same way.
- Proxies. Pass the real client IP to siteverify (from
CF-Connecting-IPorX-Forwarded-Forbehind trusted proxies). - Alternatives include hCaptcha and reCAPTCHA; the server-side verification pattern is the same.
- Disposable email domains can be blocked with a maintained list if they remain a problem.
Next steps
- Edge-level bot defences: Protect Django From Bots and Scrapers With a WAF.
- Keep your sending reputation healthy with the Django Email Deliverability Checklist.
FAQ
How do I add Cloudflare Turnstile to a Django form?
Add the widget to the template and verify the cf-turnstile-response token server-side with the siteverify API.
Do I need my site behind Cloudflare to use Turnstile?
No. Turnstile works on any site; you only need a Cloudflare account for the keys.
Are honeypot fields enough?
They stop simple bots. Combine them with Turnstile and rate limits for stronger protection.
Why require email confirmation?
It blocks fake accounts and prevents your app emailing addresses bots typed in.