Tips: Using Cloudflare Turnstile on a Astro form

Join the AI Workshop to learn more about AI and how it can be applied to web development. Next cohort February 1st, 2026

The AI-first Web Development BOOTCAMP cohort starts February 24th, 2026. 10 weeks of intensive training and hands-on projects.


Here’s how I used Cloudflare Turnstile on a Astro form to prevent spam and form submission abuse.

Set Turnstile up in the Cloudflare panel first, and grab the TURNSTILE_SITE_KEY and TURNSTILE_SITE_SECRET variables, put them in .env or anywhere you manage env vars.

Then in the Astro component:

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

<form method='post'>
 ...
	<div
	  class='cf-turnstile'
	  data-sitekey={import.meta.env
	    .TURNSTILE_SITE_KEY ||
	    process.env.TURNSTILE_SITE_KEY}>
	</div>

  <input
    type='submit'
    value='Login'
  />
</form>

On the server endpoint (might be same page, or not):

export async function processTurnstile(
  cf_turnstile_response: string
) {
  const url =
    'https://challenges.cloudflare.com/turnstile/v0/siteverify'

  const requestBody = new URLSearchParams({
    secret:
      import.meta.env.TURNSTILE_SITE_SECRET ||
      process.env.TURNSTILE_SITE_SECRET,
    response: cf_turnstile_response
  })

  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded'
    },
    body: requestBody.toString()
  })

  const data = await response.json()

  return data.success
}

if (Astro.request.method === 'POST') {
  const formData = await Astro.request.formData()

  const email = formData.get('email')?.toString() || ''
  const password =
    formData.get('password')?.toString() || ''

	const is_valid_turnstile = await processTurnstile(
    formData.get('cf-turnstile-response')?.toString() || ''
  )

  if (!is_valid_turnstile) {
    console.log('Invalid turnstile')
  } else {
		//valid, do something
  }
}

Lessons in this unit:

0: Introduction
1: Fix .md in links
2: Moving a simple site to Astro
3: Astro, fix Form error “Content-Type was not one of…”
4: Astro page layout and middleware execution order
5: Astro, set caching headers
6: Astro, set response header
7: Deploying an Astro + PostgreSQL app on Railway
8: Using Astro locals
9: ▶︎ Using Cloudflare Turnstile on a Astro form
10: Using reCAPTCHA on a Astro form
11: Why not write logic in Astro layouts