Skip to main content

Python Django: How to Resolve Error "CSRF Token Mismatch"

The "CSRF token mismatch" error is one of the most common issues developers encounter when building web applications with Django. While CSRF protection is essential for securing your application against malicious attacks, misconfiguration or missing tokens can block legitimate requests and leave you staring at a 403 Forbidden page.

In this guide, you'll learn what CSRF is, why Django enforces it, what causes the token mismatch error, and how to fix it with practical examples.

What Is CSRF?

CSRF (Cross-Site Request Forgery) is a type of web security attack where a malicious website tricks a user's browser into making an unwanted request to a different site where the user is already authenticated. For example, a hidden form on an attacker's page could submit a money transfer request to your banking site using your active session.

To prevent this, Django generates a unique, random CSRF token for each user session. This token must be included in every state-changing request (POST, PUT, DELETE). When the server receives the request, it compares the submitted token against the one stored in the session. If they don't match or if the token is missing, Django rejects the request.

What Causes the "CSRF Token Mismatch" Error?

The error occurs when Django's CsrfViewMiddleware determines that the CSRF token in the incoming request does not match the expected token for the current session. The most common causes are:

  • Missing {% csrf_token %} tag in your HTML form template.
  • CSRF middleware is not enabled in your Django settings.
  • Expired session or token: the user was inactive for too long and the session expired.
  • Cross-domain requests: submitting a form or AJAX request to a different domain without proper CSRF configuration.
  • Stale or corrupted browser cookies: old cookies conflict with the current session token.
  • AJAX requests missing the CSRF header: JavaScript requests don't automatically include the CSRF token.
  • Caching issues: a cached page serves an outdated CSRF token.

How Django CSRF Protection Works

Understanding the flow helps you debug the mismatch:

  1. When a user visits a page, Django sets a csrftoken cookie in the browser.
  2. When the page contains a form with {% csrf_token %}, Django renders a hidden input field containing the token value.
  3. When the form is submitted, Django's CsrfViewMiddleware compares the token from the form data (or the X-CSRFToken header) against the token in the cookie.
  4. If they match, the request proceeds. If not, Django returns a 403 Forbidden response with the message: "CSRF verification failed. Request aborted."

Fix 1: Include the CSRF Token in Your Form

The most common cause of this error is a missing {% csrf_token %} tag in your template.

Wrong: Missing CSRF token:

<form method="POST" action="/login/">
<input type="text" name="username" />
<input type="password" name="password" />
<button type="submit">Log In</button>
</form>

Submitting this form will produce:

Forbidden (403)
CSRF verification failed. Request aborted.

Correct: CSRF token included:

<form method="POST" action="/login/">
{% csrf_token %}
<input type="text" name="username" />
<input type="password" name="password" />
<button type="submit">Log In</button>
</form>

When you inspect the rendered HTML, you should see a hidden input field:

<input type="hidden" name="csrfmiddlewaretoken" value="a1b2c3d4e5f6...">
tip

You can verify the token is present by right-clicking the form in your browser, selecting Inspect, and looking for the csrfmiddlewaretoken hidden input.

Fix 2: Ensure CSRF Middleware Is Enabled

Django's CSRF protection is handled by CsrfViewMiddleware. If it's missing from your middleware configuration, tokens won't be validated, but they also won't be generated properly, which can cause confusing behavior.

Verify that your settings.py includes the middleware:

# settings.py

MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', # Must be present
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
warning

Never remove CsrfViewMiddleware from production settings to "fix" the error. This disables CSRF protection entirely and leaves your application vulnerable to cross-site request forgery attacks.

Fix 3: Include the CSRF Token in AJAX Requests

When making AJAX requests with JavaScript (e.g., using fetch or XMLHttpRequest), the CSRF token is not automatically included. You must extract it from the cookie and add it as a request header.

Using vanilla JavaScript with fetch:

// Helper function to read a cookie by name
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.startsWith(name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}

// Include the CSRF token in the request header
fetch('/api/update/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken'), // Required
},
body: JSON.stringify({ key: 'value' }),
})
.then(response => response.json())
.then(data => console.log(data));

Using jQuery (shortcut with $.ajaxSetup):

$.ajaxSetup({
beforeSend: function(xhr, settings) {
if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) {
xhr.setRequestHeader("X-CSRFToken", getCookie('csrftoken'));
}
}
});
info

Django's documentation provides the getCookie helper function. You can also access the token directly from the DOM if you've rendered it in a <meta> tag:

<meta name="csrf-token" content="{{ csrf_token }}">
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');

Fix 4: Clear Browser Cookies

Stale or corrupted cookies can cause a mismatch between the token stored in the cookie and the one rendered in the form. Clearing cookies for your domain often resolves this.

Steps:

  1. Open your browser's Settings (or Preferences).
  2. Navigate to Privacy & SecurityCookies and site data.
  3. Search for your domain (e.g., localhost or your production domain).
  4. Delete the cookies associated with that domain.
  5. Refresh the page and try again.

Alternatively, you can clear cookies directly from Developer Tools:

  1. Open Developer Tools (F12 or Ctrl+Shift+I).
  2. Go to the Application tab (Chrome) or Storage tab (Firefox).
  3. Under Cookies, select your domain and delete the csrftoken cookie.
  4. Reload the page.

Fix 5: Handle Expired Sessions

CSRF tokens are tied to the user's session. If the session expires (due to inactivity or a server restart), the token becomes invalid. The next form submission will fail with a mismatch error.

Solutions:

  • Increase session timeout in settings.py:
# settings.py

# Session expires after 24 hours (in seconds)
SESSION_COOKIE_AGE = 86400

# Keep the session alive as long as the user is active
SESSION_SAVE_EVERY_REQUEST = True
  • Handle the error gracefully by catching the 403 response and prompting the user to refresh:
# views.py

from django.middleware.csrf import CsrfViewMiddleware
from django.http import HttpResponseForbidden

# In your custom 403 handler:
def csrf_failure(request, reason=""):
return HttpResponseForbidden(
"Your session has expired. Please refresh the page and try again."
)

Register the custom handler in settings.py:

# settings.py
CSRF_FAILURE_VIEW = 'myapp.views.csrf_failure'

Fix 6: Configure CSRF for Cross-Domain Requests

If your frontend and backend are on different domains (e.g., a React frontend on localhost:3000 and a Django API on localhost:8000), you need to configure Django to accept CSRF tokens from the frontend's origin.

# settings.py

# Allow the frontend domain to send CSRF cookies
CSRF_TRUSTED_ORIGINS = [
'http://localhost:3000',
'https://yourfrontend.com',
]

# If using cookies across domains
CSRF_COOKIE_SAMESITE = 'Lax' # or 'None' for cross-site (requires HTTPS)
CSRF_COOKIE_HTTPONLY = False # Must be False so JavaScript can read the token
warning

Setting CSRF_COOKIE_SAMESITE = 'None' requires CSRF_COOKIE_SECURE = True, which means your site must be served over HTTPS. Browsers will reject SameSite=None cookies without the Secure flag.

Fix 7: Temporarily Disable CSRF for Debugging (Development Only)

If you need to isolate whether CSRF is the actual cause of your issue, you can temporarily exempt a specific view using the @csrf_exempt decorator:

from django.views.decorators.csrf import csrf_exempt
from django.http import JsonResponse

@csrf_exempt
def my_api_view(request):
if request.method == 'POST':
return JsonResponse({"status": "success", "message": "CSRF bypassed"})
return JsonResponse({"status": "ready"})
danger

Never use @csrf_exempt in production unless the view is explicitly designed to receive requests from external services (e.g., webhooks) and you have implemented alternative authentication (such as API tokens or HMAC signatures). Disabling CSRF protection exposes the endpoint to cross-site request forgery attacks.

Debugging Checklist

If you're still encountering the error after trying the fixes above, work through this checklist:

CheckHow to Verify
{% csrf_token %} in form templateInspect the rendered HTML for the hidden input
CsrfViewMiddleware in MIDDLEWAREReview settings.py
CSRF token in AJAX headersCheck the X-CSRFToken header in Developer Tools → Network tab
csrftoken cookie existsDeveloper Tools → Application → Cookies
Cookie not expiredDelete and regenerate by refreshing the page
Cross-domain origins trustedCheck CSRF_TRUSTED_ORIGINS in settings.py
No caching of CSRF-protected pagesEnsure Cache-Control: no-cache headers are set
JavaScript console errorsDeveloper Tools → Console tab

Conclusion

The "CSRF token mismatch" error in Django is a security feature working as intended, i.e. it's telling you that the token submitted with the request doesn't match what the server expects. In most cases, the fix is straightforward:

  1. Add {% csrf_token %} to every POST form in your templates.
  2. Include the X-CSRFToken header in AJAX requests.
  3. Clear stale browser cookies if the token has become corrupted.
  4. Configure CSRF_TRUSTED_ORIGINS for cross-domain setups.

Resist the temptation to disable CSRF protection globally. Instead, understand the flow (cookie, hidden field, middleware comparison) and you'll be able to diagnose and fix any CSRF-related issue quickly and securely.