How to Add CAPTCHA to Django Forms

Blog / Django · August 29, 2020 · Updated June 10, 2026 · 8 min read
How to Add CAPTCHA to Django Forms

To add CAPTCHA to a Django form in 2026, install the maintained django-recaptcha package, set your RECAPTCHA_PUBLIC_KEY and RECAPTCHA_PRIVATE_KEY in settings.py, and add a ReCaptchaField (with a ReCaptchaV2Checkbox or ReCaptchaV3 widget) to your form. Django then renders the widget and verifies the token with Google on the server during form.is_valid() — no manual API calls needed.

The old python-recaptcha client used in the original version of this article is unmaintained and built for a reCAPTCHA API that no longer exists. This rewrite covers the current approach for Django 5.x, plus the modern privacy-first alternatives Cloudflare Turnstile and hCaptcha.

Key takeaways

  • Use the actively maintained django-recaptcha package (PyPI), not the abandoned python-recaptcha.
  • reCAPTCHA v2 shows a checkbox/puzzle; reCAPTCHA v3 is invisible and returns a 0.0–1.0 score you threshold server-side.
  • Always verify the token on the server — a client-side widget alone blocks nothing. With django-recaptcha, form.is_valid() does this for you.
  • Cloudflare Turnstile is a free, privacy-first option with no image puzzles; MicroPyramid runs it on our own contact form.
  • Layer CAPTCHA with rate limiting and a honeypot field for real-world spam resistance, and keep the flow accessible.

Which CAPTCHA should you use?

There is no single best choice — it depends on how much friction you can accept and how privacy-sensitive your audience is. The table below compares the four options most Django teams reach for in 2026. (Cost columns are each vendor’s own published pricing, for context only.)

Solution CAPTCHA type UX friction Privacy Cost model (vendor)
reCAPTCHA v2 (checkbox) "I’m not a robot" checkbox + image puzzle fallback Medium Sends signals to Google Free up to ~1M assessments/month
reCAPTCHA v3 Invisible, score-based (0.0–1.0) None (no challenge shown) Runs on every page; Google data sharing Free up to ~1M assessments/month
hCaptcha Checkbox + image puzzle Medium Privacy-focused, GDPR-friendly Free tier; paid Enterprise plans
Cloudflare Turnstile Managed / non-interactive, no puzzles Very low Privacy-first, no personal-data selling Free, unlimited

Quick rule of thumb: choose reCAPTCHA v2 when you want a visible, familiar challenge; reCAPTCHA v3 when you want zero friction and are comfortable thresholding a score; hCaptcha or Cloudflare Turnstile when privacy and a clean UX matter most. The rest of this guide implements reCAPTCHA v2 and v3 with django-recaptcha, then shows a Turnstile integration.

Step 1: Install django-recaptcha

django-recaptcha is the de-facto Django integration for Google reCAPTCHA. It ships form fields and widgets for v2 (checkbox and invisible) and v3, and handles server-side verification for you. Install it with pip:

pip install django-recaptcha

Then add the app to INSTALLED_APPS. Note the import name changed in version 4: the app label is now django_recaptcha (older tutorials use captcha, which no longer applies).

# settings.py
INSTALLED_APPS = [
    # ...
    'django_recaptcha',
]

Step 2: Configure your keys in settings.py

Create a reCAPTCHA site in the Google reCAPTCHA admin console, choose v2 or v3, and register your domain. You get a site key (public) and a secret key (private). Store them in settings.py — ideally read from environment variables, never hard-coded in source control.

# settings.py
import os

RECAPTCHA_PUBLIC_KEY = os.environ['RECAPTCHA_PUBLIC_KEY']   # site key
RECAPTCHA_PRIVATE_KEY = os.environ['RECAPTCHA_PRIVATE_KEY'] # secret key

# Optional: global pass threshold for reCAPTCHA v3 (default 0.5)
RECAPTCHA_REQUIRED_SCORE = 0.85

During local development you can use Google’s public test keys, which always validate. They emit a warning at startup; silence it with SILENCED_SYSTEM_CHECKS:

# settings.py (LOCAL/DEV ONLY — never ship these to production)
RECAPTCHA_PUBLIC_KEY = '6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI'
RECAPTCHA_PRIVATE_KEY = '6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe'
SILENCED_SYSTEM_CHECKS = ['django_recaptcha.recaptcha_test_key_error']

Step 3: Add ReCaptchaField to a form (reCAPTCHA v2)

Add a ReCaptchaField to any Django Form or ModelForm. For the classic "I’m not a robot" checkbox, pair it with the ReCaptchaV2Checkbox widget. If you prefer the badge-only experience, use ReCaptchaV2Invisible instead. If you are new to Django forms, our Django forms basics guide covers fields, widgets, and validation.

# forms.py
from django import forms
from django_recaptcha.fields import ReCaptchaField
from django_recaptcha.widgets import ReCaptchaV2Checkbox


class ContactForm(forms.Form):
    name = forms.CharField(max_length=100)
    email = forms.EmailField()
    message = forms.CharField(widget=forms.Textarea)
    captcha = ReCaptchaField(widget=ReCaptchaV2Checkbox())

Render the form normally in your template. The field outputs the reCAPTCHA widget and the required script tag automatically:

<!-- templates/contact.html -->
<form method="post">
  {% csrf_token %}
  {{ form.as_p }}
  <button type="submit">Send</button>
</form>

The view needs no special CAPTCHA logic. Because ReCaptchaField validates the token against Google’s API inside the field’s clean() method, a successful form.is_valid() already means the CAPTCHA passed — verification happens server-side, automatically.

# views.py
from django.shortcuts import render, redirect
from .forms import ContactForm


def contact(request):
    if request.method == 'POST':
        form = ContactForm(request.POST)
        if form.is_valid():  # CAPTCHA is verified here, server-side
            # ... handle the cleaned data (save, email, etc.)
            return redirect('contact_thanks')
    else:
        form = ContactForm()
    return render(request, 'contact.html', {'form': form})

reCAPTCHA v3: invisible, score-based

reCAPTCHA v3 never interrupts the user. Instead it returns a score from 0.0 (likely a bot) to 1.0 (likely human) on every request, and you decide the cut-off. Swap the widget to ReCaptchaV3 and optionally set a per-field threshold via the widget’s required_score attribute (this overrides the global RECAPTCHA_REQUIRED_SCORE).

# forms.py (reCAPTCHA v3)
from django import forms
from django_recaptcha.fields import ReCaptchaField
from django_recaptcha.widgets import ReCaptchaV3


class SignupForm(forms.Form):
    email = forms.EmailField()
    # Reject anything scoring below 0.85 as suspected bot traffic
    captcha = ReCaptchaField(widget=ReCaptchaV3(attrs={'required_score': 0.85}))

Tune the threshold to your traffic: 0.5 is Google’s default, 0.7–0.9 is common for high-value forms (signup, checkout). Too high and you reject real users; too low and bots slip through. Because v3 is silent, log borderline scores and watch your false-positive rate before tightening it.

Cloudflare Turnstile and hCaptcha

If you would rather not send signals to Google, Cloudflare Turnstile and hCaptcha are strong, privacy-friendly alternatives. Turnstile is free and shows no image puzzles — it runs a managed challenge that is invisible to most users. At MicroPyramid we use Cloudflare Turnstile on our own contact form alongside rate limiting and a honeypot, and it has cut bot submissions dramatically without annoying real visitors.

Turnstile has no first-party Django form field, so you render its widget in the template and verify the cf-turnstile-response token server-side via Cloudflare’s siteverify endpoint. The same pattern (POST the token to the provider, check success) applies to any provider you wire up by hand.

# views.py — verifying a Cloudflare Turnstile token server-side
import requests
from django.conf import settings


def verify_turnstile(token, remote_ip=None):
    """Return True only if Cloudflare confirms the token is valid."""
    resp = requests.post(
        'https://challenges.cloudflare.com/turnstile/v0/siteverify',
        data={
            'secret': settings.TURNSTILE_SECRET_KEY,
            'response': token,
            'remoteip': remote_ip,
        },
        timeout=5,
    )
    return resp.json().get('success', False)


def contact(request):
    if request.method == 'POST':
        token = request.POST.get('cf-turnstile-response')
        ip = request.META.get('REMOTE_ADDR')
        if not verify_turnstile(token, ip):
            return render(request, 'contact.html', {'error': 'CAPTCHA failed'})
        # ... token is valid; process the submission
    return render(request, 'contact.html')

For hCaptcha, the community django-hcaptcha package mirrors the django-recaptcha API (HCaptchaField), so the form-field approach above carries over almost unchanged.

Server-side verification is mandatory

The single most important rule: never trust the client. A CAPTCHA widget rendered in the browser proves nothing on its own — an attacker can submit your form directly, skipping the JavaScript entirely. Security only comes from your server confirming the token with the provider before accepting the data. With django-recaptcha this happens automatically inside form.is_valid(); with a hand-rolled provider you must call its verify endpoint yourself, as shown above.

Layer your defenses

CAPTCHA is one layer, not a complete solution. Combine it with:

  • Rate limiting — cap submissions per IP per hour (e.g. django-ratelimit) so a single source cannot hammer the form.
  • A honeypot field — a hidden input real users never fill; if it arrives populated, it is a bot. This catches naive scripts before a CAPTCHA round-trip is even needed.
  • Server-side validation — strict email/length checks and disposable-domain filtering.
  • Authorization checks — for gated actions, verify the user actually has permission; see our guides on custom decorators for user roles and permissions.
  • Monitoring — watch for spikes in failed submissions. Wiring Django up to Sentry gives you live visibility into abuse and errors.

Keep it accessible

Image-puzzle CAPTCHAs (reCAPTCHA v2, hCaptcha) can block users who rely on screen readers or have low vision. Favour score-based reCAPTCHA v3 or Cloudflare Turnstile, which usually require no interaction, always provide a clear error message when a challenge fails, and make sure keyboard navigation reaches the widget. Accessibility is a legal as well as an ethical requirement in many markets.

Need help hardening forms, auth, or anti-spam in a production Django app? MicroPyramid has shipped 50+ projects over 12+ years and offers Django development services for teams that want it done right.

Frequently Asked Questions

Which CAPTCHA is best for Django in 2026?

For most Django sites, Cloudflare Turnstile or reCAPTCHA v3 give the best balance of protection and user experience because they require no image puzzles. Use reCAPTCHA v2 when you specifically want a visible "I’m not a robot" challenge, and hCaptcha when GDPR-friendly privacy is a top priority. All four are free for typical traffic volumes.

Is django-recaptcha still maintained?

Yes. django-recaptcha (version 4.x) is the actively maintained package and the standard way to add Google reCAPTCHA to Django. The older python-recaptcha client referenced in many old tutorials is abandoned and targets a removed API, so you should not use it.

Do I need to verify the CAPTCHA on the server?

Always. A CAPTCHA in the browser can be bypassed by posting directly to your endpoint, so the token must be confirmed with the provider server-side. With django-recaptcha, form.is_valid() performs this verification automatically; for Cloudflare Turnstile or hCaptcha you call the provider’s siteverify endpoint yourself.

What is a good reCAPTCHA v3 score threshold?

reCAPTCHA v3 returns a score from 0.0 (bot) to 1.0 (human). Google’s default cut-off is 0.5. A common range for important forms is 0.7 to 0.9 — higher blocks more bots but risks rejecting real users. Start at 0.5, log the scores you see, and tighten gradually based on your false-positive rate.

How do I add Cloudflare Turnstile to a Django form?

Add the Turnstile widget script and <div class="cf-turnstile"> to your template, then in your view read the cf-turnstile-response field and POST it with your secret key to https://challenges.cloudflare.com/turnstile/v0/siteverify. Accept the submission only when the JSON response has success: true.

Are CAPTCHAs accessible to users with disabilities?

Image and audio puzzles can be a barrier for people using screen readers or with low vision. To stay accessible, prefer no-interaction options like reCAPTCHA v3 or Cloudflare Turnstile, ensure the widget is keyboard-reachable, and show a clear, descriptive error when verification fails so users know what to do next.

Share this article