Caching FormsImproved in 5.0+
Forms and pages can be cached in a variety of ways, but you need to ensure that your CSRF token is being refreshed in order for the form to continue working.
Here are some solutions, depending on your caching approach:
Craft Caching
When using simple Twig-based Craft Caching, you'll need to make sure that you are refreshing the CSRF token.
There are two approaches you can take:
- Craft's `asyncCsrfInputs` setting
- Manually Refresh CSRF token
If you're using Freeform 5.11+ and AJAX, enable Craft's asyncCsrfInputs setting and let Freeform handle CSRF auto-fetch (no custom endpoint needed).Recommended
If you're running an older version of Freeform, or have custom JavaScript submitting POST requests outside of Freeform's request layer, you'll need to implement a CSRF token refresh strategy. You can do this using either of the following approaches:
- Craft's
/actions/users/session-infoendpoint (simplest to implement) - A custom Twig-based JSON endpoint (offers more control and a smaller payload)
Template Code
{% cache %}
{{ freeform.form("myFormHandle").render }}
{% endcache %}
Craft Config
- Static Config
- Environment Override
->asyncCsrfInputs(true)
CRAFT_ASYNC_CSRF_INPUTS=true
If you need wish to refresh the CSRF token manually instead, this can be done with a bit of javascript in the template:
{# Initialize the form #}
{% set form = freeform.form("myFormHandle") %}
{# Cached form comes here #}
{% cache %}
{{ form.render }}
{% endcache %}
{# Script for updating the form's CSRF token #}
<script>
// Find the corresponding Form
var form = document.querySelector('form');
// Locate and update the CSRF input
var csrfInput = form.querySelector('input[name={{ craft.app.config.general.csrfTokenName|e('js') }}]');
csrfInput.value = '{{ craft.app.request.csrfToken|e('js') }}';
</script>
Static Caching / Blitz
When using full static page caching with services like CloudFlare, Craft Cloud or the Blitz Craft plugin, you'll need to make sure that you are refreshing the CSRF token.
There are a few different approaches you can take:
- Single-Page forms
- Single & Multi-Page forms
- Blitz `csrfInput()`
If using a single-page form and you're using Freeform 5.11+ and AJAX, enable Craft's asyncCsrfInputs setting and let Freeform handle CSRF auto-fetch (no custom endpoint needed).Recommended
If you're running an older version of Freeform, or have custom JavaScript submitting POST requests outside of Freeform's request layer, you'll need to implement a CSRF token refresh strategy. You can do this using either of the following approaches:
- Craft's
/actions/users/session-infoendpoint (simplest to implement) - A custom Twig-based JSON endpoint (offers more control and a smaller payload)
Craft Config
- Static Config
- Environment Override
->asyncCsrfInputs(true)
CRAFT_ASYNC_CSRF_INPUTS=true
Craft's asyncCsrfInputs setting will not work correctly on multi-page forms at this time. Instead, you'll need to manually refresh the CSRF token in your template.
Your forms must have AJAX enabled for the form in order for this to work correctly.
Create a separate Twig template to handle loading the refreshed tokens with the following code:
{{
{
csrf: {
name: craft.app.config.general.csrfTokenName,
value: craft.app.request.csrfToken
}
}
|json_encode|raw
}}
Make sure routing to that template works correctly. Best practice is to dedicate a directory inside the Craft templates directory named something like dynamic and place files in that directory that should never be cached. The above template might be in a directory called dynamic with a filename of index.twig.
Set up a rule in Blitz, CloudFlare or your preferred CDN that makes sure any URLs starting with dynamic are not cached. You can then aggressively cache all of your other site URLs.
Add the following JavaScript snippet to your main .js file, or just before the closing </body> tag on your site's pages.
This script uses AJAX to request https://yourwebsite.com/dynamic, which returns a fresh CSRF token. The token is then automatically injected into the CSRF fields of all forms on the page.
Be sure to configure your caching layer (e.g. Blitz, Cloudflare, or your preferred CDN) to bypass caching for any URLs beginning with /dynamic. This ensures that CSRF tokens are always generated per-request, while allowing the rest of your site to remain aggressively cached. If your firewall blocks Craft's /actions/* endpoints, you may also need to explicitly allow /dynamic/* in your Cloudflare WAF security rules.
- Plain JS
- jQuery
document.addEventListener("DOMContentLoaded", function () {
// Find the corresponding form
var form = document.querySelector('form');
if (!form) return;
// Fetch the new CSRF token
fetch('https://yourwebsite.com/dynamic')
.then(response => response.json())
.then(data => {
if (data.csrf && data.csrf.name && data.csrf.value) {
var csrfInput = form.querySelector('input[name="' + data.csrf.name + '"]');
if (csrfInput) {
csrfInput.value = data.csrf.value;
}
}
})
.catch(error => console.error("CSRF update error:", error
));
});
$(function () {
// Find the corresponding Form
var form = document.querySelector('form');
$.ajax({
// Specify the form handle in the GET parameters
// ! Make sure to change the `myFormHandle` to your specific form handle.
url: 'https://yourwebsite.com/dynamic',
type: 'get',
dataType: 'json',
success: function (response) {
// Locate and update the CSRF input
var csrf = response.csrf;
form.querySelector('input[name=' + csrf.name + ']').value = csrf.value;
},
});
});
Note that this snippet assumes you're already running jQuery.
Your final template code might look something like this:
{# Initialize the form #}
{% set form = freeform.form("myFormHandle") %}
{{ form.render }}
{# Load the jQuery library, if using #}
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
{# The script for updating the form's CSRF token loads after #}
<script src="/assets/js/main.js"></script>
Another option is to use Blitz's CSRF Token Input tag. However, this method requires coding your form formatting inside your template rather than using a formatting template, which can be quite inconvenient.
{# Initialize the form #}
{% set form = freeform.form("myFormHandle") %}
{{ form.renderTag }}
{# Add Blitz's CSRF token input inside <form> tags #}
{{ craft.blitz.csrfInput() }}
{% if form.hasErrors %}
<div class="freeform-form-errors">
{{ "Error! Please review the form and try submitting again."|t('freeform') }}
</div>
{% endif %}
{% for row in form %}
<div class="field-row">
{% for field in row %}
<div class="field-container">
{{ field.render() }}
</div>
{% endfor %}
</div>
{% endfor %}
{{ form.renderClosingTag }}
Stripe Payment Forms
When using the built-in Stripe integration, Freeform will trigger Payment Intent requests automatically upon page load. These requests use the initial form data, meaning the original CSRF token.
The assumption is that payment forms should never be cached, so if your form has a Stripe integration enabled and you are using Blitz caching and refreshing your CSRF tokens, we suggest injecting your payment form into your template using Blitz dynamic content or Sprig.
There is then no need to refresh CSRF tokens as the dynamic content is injected after the page loads. Otherwise, you risk a 400 Bad Request - Unable to verify your data submission error.
Craft Cloud + URL Tracking Parameters
When using static caching in Craft Cloud, you may encounter an issue where tracked URL parameters (such as UTM values like ?utm_source=google) become unintentionally cached into the page output.
Craft Cloud's edge caching may cache the fully rendered HTML of a page after Freeform has already populated tracked URL parameter values into the form.
For example:
- A visitor arrives via a campaign link such as:
/contact?utm_source=google - Freeform tracks the
utm_sourcevalue and populates it into the form. - Craft Cloud caches the rendered page, including that tracked value.
- Subsequent visitors (without any UTM parameters) may now receive the cached version of the page containing the original visitor's tracked values.
This can result in incorrect tracking data being submitted by other users.
There are a few different approaches available depending on your performance needs:
- Prevent the Form Page from Being Cached
- Use Edge-Side Includes (Recommended)
- Ignore Tracking Parameters in the Cache Key
Option 1: Prevent the Form Page from Being Cached
The simplest solution is to prevent Craft Cloud from statically caching the page that contains your form.
You can do this by adding the following Twig tag anywhere in the template that renders your Freeform form:
{% expires %}
This ensures the page is always rendered dynamically and prevents tracked URL parameter values from being cached into the HTML.
Option 2: Use Edge-Side Includes (Recommended)
Craft Cloud supports Edge-Side Includes (ESI), which allow you to keep the main page cached while rendering specific portions dynamically.
This provides the best balance of performance and accuracy by keeping your form output uncached while the rest of the page remains statically cached.
Update Your Main Template
Replace your form include with an ESI call:
{{ cloud.esi('contact-form.twig') }}
Create a Dedicated Form Template
Move your Freeform form rendering into a separate template (e.g. contact-form.twig) and add the following:
{% expires %}
{{ freeform.form('contact').render() }}
This ensures that only the form is dynamically rendered at request time, preventing tracked URL parameters from being cached and reused across visitors.
Option 3: Ignore Tracking Parameters in the Cache Key
In addition to rendering your form dynamically, you may also wish to configure Craft Cloud to ignore common marketing query parameters (such as UTM values) when generating the cache key.
Without this, crawlers or campaign visitors may unintentionally generate cached versions of your page that include tracking parameters, even if they never submit the form.
By configuring Craft Cloud to ignore parameters such as:
utm_source
utm_medium
utm_campaign
utm_term
utm_content
gclid
fbclid
You can help prevent multiple cached variants of the same page from being created based on marketing or referral data. This reduces the risk of cached tracking values being embedded into statically cached HTML and served to other visitors. Refer to the Craft Cloud documentation for details on configuring ignored query parameters for caching.