Blog

Implementing Bot Protection on Contact Forms with Cloudflare Turnstile

On Thursday, Dec 18, 2025
post image

Submissions like “LroQQAakgtRqFvwVsJU” from random free email addresses started flooding our contact form. Each nonsense submission meant time wasted validating leads, cluttered notification channels, and genuine inquiries buried under noise. We needed a solution that would stop bots without creating friction for the people actually trying to reach us.

In this post, we’ll walk through why contact forms attract bots, how we evaluated bot protection options, and the technical implementation of Cloudflare Turnstile that eliminated our spam problem. We’ll cover the privacy considerations that influenced our choice, share the actual code we deployed, and discuss what we learned from early results.

The Bot Traffic Problem

If gibberish form submissions feel like a growing problem, that’s because they are. According to Imperva’s 2024 Bad Bot Report, bad bots account for 32% of all internet traffic—and that number has been rising for five consecutive years. Nearly half of all web traffic now comes from automated sources rather than humans.

For businesses relying on contact forms for lead generation, this creates a specific operational burden. Every bot submission demands attention until someone manually determines it’s fake. That time adds up quickly when you’re trying to respond to legitimate inquiries within hours, not days. Beyond the time cost, bot spam makes it harder to spot patterns in genuine leads and can skew analytics about which marketing channels drive actual interest.

How Bots Target Contact Forms

Bots don’t stumble onto your contact form by accident. They actively search for them using automated crawlers that scan HTML code looking for form elements. When they find input fields with names like “email,” “message,” or “name,” they populate those fields with pre-programmed data and submit at machine speed—far faster than any human could type.

Contact forms are particularly attractive targets because they represent direct communication channels to businesses. Bots exploit this access for several purposes: harvesting email addresses and personal data for resale, injecting malicious links that could compromise websites or spread malware, inserting backlinks to manipulate search engine rankings, and probing for vulnerabilities in form handling logic.

Some bot submissions use completely fabricated data like our “LroQQAakgtRqFvwVsJU” example. Others leverage stolen credentials or real email addresses harvested from data breaches, making them harder to identify at first glance. The sophistication varies, but the impact is consistent: noise that obscures signal.

Evaluating Bot Protection Options

We needed a solution that would verify human users without adding friction to our form submission process. The comparison quickly narrowed to two options: Google’s reCAPTCHA and Cloudflare Turnstile. Both can effectively distinguish humans from bots. Both offer invisible or minimal-interaction modes that don’t force users to solve puzzles or identify traffic lights. The meaningful differences emerged around privacy, integration complexity, and cost structure.

Privacy Considerations

reCAPTCHA works by collecting browsing behavior data, mouse movements, IP addresses, and device fingerprinting information to assess whether a user is human. While Google states this data isn’t used for ad targeting, the collection itself raises considerations under privacy regulations like GDPR and CCPA.

Cloudflare Turnstile takes a different approach. It examines session data like headers, user agent, and browser characteristics to verify users, but it doesn’t use cookies, doesn’t track users across sites, and explicitly doesn’t collect data for advertising purposes. This design philosophy gave us a cleaner privacy posture without introducing additional tracking mechanisms to our site.

Integration and Cost

We were already hosting our static site on Cloudflare Pages, which made Turnstile a natural fit. No new vendor relationship, no additional third-party JavaScript from Google’s domain, and direct integration with our existing Cloudflare Pages Functions for backend form processing.

Cost also played a role. Turnstile is free for our use case—and for most websites. reCAPTCHA recently introduced pricing tiers that, while reasonable for many businesses, represented an additional line item we could avoid.

The deciding factors: privacy-focused design, seamless integration with our existing infrastructure, and zero additional cost. Turnstile aligned with what we needed.

Implementation

The implementation consisted of three components: adding the Turnstile widget to our existing contact form template, backend verification in our Cloudflare Pages Function, and client-side JavaScript to manage button state based on verification status.

Frontend Integration

We added the Turnstile widget to our existing contact form with callback functions to handle different verification states:

 
<!-- Turnstile widget -->
<div class="cf-turnstile mb-6"
    data-sitekey="1x00000000000000000000AA"
    data-callback="onTurnstileSuccess"
    data-expired-callback="onTurnstileExpired"
    data-error-callback="onTurnstileError"
    data-theme="light">
</div>

<div>
    <button id="form-submit-button" disabled="disabled" type="submit" 
            class="text-lg bg-blue-500 rounded-full hover:scale-105 hover:brightness-110 
                   text-white py-2 px-4 transition-all cursor-pointer disabled:opacity-50">
        Contact Me
    </button>
</div>  

The submit button starts in a disabled state. This prevents users from submitting the form before Turnstile completes its verification and prevents unnecessary function calls from incomplete submissions.

The Turnstile script needs to be included in your page’s <head>:

 
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>

Button State Management

Three callback functions control button availability based on Turnstile’s verification state:

 
  // CF Turnstile callbacks
  const submitButton = document.getElementById('form-submit-button');
  
  function onTurnstileSuccess(token) {
      submitButton.disabled = false;
  }
  
  function onTurnstileExpired() {
      submitButton.disabled = true;
  }
  
  function onTurnstileError() {
      submitButton.disabled = true;
  }

When Turnstile successfully verifies the user (or determines no challenge is needed), onTurnstileSuccess enables the submit button. If the verification expires before submission or encounters an error, the button is disabled again. This keeps the user experience clean while ensuring we only process verified submissions.

Backend Verification

The Cloudflare Pages Function handles server-side verification before processing any form submission. This is crucial—client-side verification alone can be bypassed:

 
export async function onRequest(context) {
  const { request, env } = context;
  const TARGET_URL = env.FORM_TARGET_URL;
  const TURNSTILE_SECRET_KEY = env.TURNSTILE_SECRET_KEY;

  try {
    const form = await request.formData();
    let formData = Object.fromEntries(form.entries());
    const turnstileToken = formData['cf-turnstile-response'];

    // Require token presence
    if (!turnstileToken) {
      return new Response(JSON.stringify({
        success: false,
        message: 'CAPTCHA verification failed. Please try again.'
      }), {
        status: 400,
        headers: {
          'Content-Type': 'application/json',
          'Access-Control-Allow-Origin': '*'
        }
      });
    }

    // Verify token with Cloudflare
    const verifyFormData = new FormData();
    verifyFormData.append('secret', TURNSTILE_SECRET_KEY);
    verifyFormData.append('response', turnstileToken);

    const verifyResponse = await fetch(
      'https://challenges.cloudflare.com/turnstile/v0/siteverify',
      {
        method: 'POST',
        body: verifyFormData,
      }
    );

    const verifyResult = await verifyResponse.json();

    if (!verifyResult.success) {
      console.error('Turnstile verification failed:', verifyResult);
      return new Response(JSON.stringify({
        success: false,
        message: 'CAPTCHA verification failed. Please try again.'
      }), {
        status: 403,
        headers: {
          'Content-Type': 'application/json',
          'Access-Control-Allow-Origin': '*'
        }
      });
    }

    // Token verified - forward to webhook
    const response = await fetch(TARGET_URL, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
      },
      body: internalMessage, // Your formatted form data
    });

    // Handle response...
  } catch (error) {
    console.error('Form proxy error:', error);
    return new Response(JSON.stringify({
      success: false,
      message: 'An error occurred while processing your form.'
    }), {
      status: 500,
      headers: {
        'Content-Type': 'application/json',
        'Access-Control-Allow-Origin': '*'
      }
    });
  }
} 

Environment Configuration

Turnstile requires two keys: a public site key (embedded in your frontend code) and a secret key (used for backend verification). We added the secret key to Cloudflare Pages environment variables as TURNSTILE_SECRET_KEY. For local development with Wrangler, Turnstile provides test keys that always pass, always fail, or present interactive challenges.

The Complete Flow

Here’s how everything works together:

activity-diagram-describing-the-complete-flow

The user fills out the form while Turnstile quietly verifies their session in the background. Most users never see a challenge—Turnstile determines they’re human based on browser signals and enables the submit button automatically. When needed, Turnstile presents a minimal challenge.

Upon submission, the browser sends both the form data and the Turnstile token to our Cloudflare Pages Function. The function verifies the token server-side with Turnstile’s API before forwarding the notification to our webhook receiver. This two-layer verification—client-side enablement plus server-side validation—prevents both accidental incomplete submissions and deliberate bypass attempts.

Testing Approach

Before deploying to production, we tested three scenarios using Turnstile’s test keys with Wrangler’s local development server:

  • Auto-pass test key verified the happy path—forms submit successfully, notifications arrive as expected, and users see confirmation messages.

  • Always-fail test key confirmed error handling works correctly. The backend properly rejects invalid tokens and returns appropriate error responses without triggering webhook notifications.

  • Interactive challenge key let us experience the actual CAPTCHA flow that some users might encounter. Even in challenge mode, the interaction is minimal compared to traditional CAPTCHAs.

The button disable/enable logic proved particularly important during testing. It prevents users from clicking submit before verification completes and eliminates unnecessary function calls from incomplete submissions—a small detail that improves both user experience and function efficiency.

Early Results and Observations

We deployed this implementation a few days ago and tested it thoroughly to ensure form submissions work correctly for legitimate users. The results have been exactly what we hoped for: zero bot submissions since deployment while maintaining smooth experiences for actual inquiries.

The notification system that previously alerted us to spam multiple times per day has only delivered notifications from legitimate form submissions. Our verification logs show clean, successful token validations with no bot attempts cluttering our monitoring.

While a few days is a short timeframe, the pattern is clear: bots blocked, humans unaffected, and the signal-to-noise problem solved.

Key Takeaways

Effective bot protection doesn’t require complex implementations or expensive third-party services. When you’re already running on infrastructure that offers integrated solutions, choosing tools that fit your existing stack makes implementation faster and maintenance simpler.

In about three hours of total implementation time—including testing—we eliminated bot spam while maintaining zero friction for legitimate leads. The ROI isn’t in complexity—it’s in choosing solutions that integrate naturally with the infrastructure you’re already using.

References

Share this post: