How to Add Bot Detection
to Django with Middleware
Django's middleware system is one of the framework's most powerful features — a chain of hooks that process every request and response flowing through your application. It's also the ideal place to add bot detection, because you can intercept malicious traffic before it reaches your views, forms, or API endpoints.
In this guide, you'll build a production-ready bot detection middleware for Django using IPASIS IP intelligence. We'll start with a basic implementation and progressively add caching, view-level decorators, Django REST Framework integration, admin panel protection, and structured logging.
What You'll Build
- ✅ Django middleware for IP risk scoring
- ✅ Django cache framework integration (memcached / Redis)
- ✅ View decorators for route-specific protection
- ✅ Django REST Framework permission class
- ✅ Admin panel brute-force protection
- ✅ Class-based view mixin
- ✅ Celery async risk scoring for non-blocking checks
- ✅ Structured security logging with django-structlog
- ✅ Complete production-ready Django app
Prerequisites
- • Django 4.2+ (works with Django 5.x too)
- • Python 3.10+
- • An IPASIS API key (free tier available)
- •
requestsorhttpxlibrary
Step 1: Basic Bot Detection Middleware
Django middleware classes implement __call__ (or the older process_request / process_response hooks). Our middleware will intercept incoming requests, extract the client IP, query IPASIS for a risk score, and either block or annotate the request.
# botdetection/middleware.py
import requests
from django.http import JsonResponse
from django.conf import settings
IPASIS_API_URL = "https://api.ipasis.com/v1/ip/{ip}"
class BotDetectionMiddleware:
"""
Django middleware that checks every request against
IPASIS IP intelligence API and blocks high-risk traffic.
"""
def __init__(self, get_response):
self.get_response = get_response
self.api_key = getattr(settings, 'IPASIS_API_KEY', '')
def __call__(self, request):
ip = self.get_client_ip(request)
try:
result = requests.get(
IPASIS_API_URL.format(ip=ip),
headers={"Authorization": f"Bearer {self.api_key}"},
timeout=2,
)
data = result.json()
except Exception:
# Fail open — don't block on API errors
data = {}
risk_score = data.get("risk_score", 0)
request.ip_risk = data # Attach to request for views
if risk_score >= 90:
return JsonResponse(
{"error": "Request blocked", "reason": "suspicious_ip"},
status=403,
)
return self.get_response(request)
@staticmethod
def get_client_ip(request):
"""Extract real client IP, respecting reverse proxies."""
xff = request.META.get("HTTP_X_FORWARDED_FOR")
if xff:
return xff.split(",")[0].strip()
return request.META.get("REMOTE_ADDR", "0.0.0.0")Register it in settings.py:
# settings.py
IPASIS_API_KEY = "your-api-key-here"
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"botdetection.middleware.BotDetectionMiddleware", # Early in chain
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
# ... rest of middleware
]Place it early in the middleware chain — after SecurityMiddleware but before session/auth middleware. This way you block bots before Django wastes resources loading sessions or authenticating.
Step 2: Add Caching with Django's Cache Framework
Calling an external API on every request is expensive and adds latency. Django's built-in cache framework gives us a clean abstraction — whether you use Redis, Memcached, or local memory, the code stays the same.
# botdetection/middleware.py (updated)
import hashlib
import requests
from django.core.cache import cache
from django.http import JsonResponse
from django.conf import settings
IPASIS_API_URL = "https://api.ipasis.com/v1/ip/{ip}"
CACHE_TTL = 300 # 5 minutes
CACHE_PREFIX = "ipasis_risk:"
class BotDetectionMiddleware:
def __init__(self, get_response):
self.get_response = get_response
self.api_key = getattr(settings, 'IPASIS_API_KEY', '')
def __call__(self, request):
ip = self.get_client_ip(request)
cache_key = f"{CACHE_PREFIX}{ip}"
# Check cache first
data = cache.get(cache_key)
if data is None:
try:
result = requests.get(
IPASIS_API_URL.format(ip=ip),
headers={"Authorization": f"Bearer {self.api_key}"},
timeout=2,
)
data = result.json()
cache.set(cache_key, data, CACHE_TTL)
except Exception:
data = {}
risk_score = data.get("risk_score", 0)
request.ip_risk = data
if risk_score >= 90:
return JsonResponse(
{"error": "Request blocked", "reason": "suspicious_ip"},
status=403,
)
return self.get_response(request)
@staticmethod
def get_client_ip(request):
xff = request.META.get("HTTP_X_FORWARDED_FOR")
if xff:
return xff.split(",")[0].strip()
return request.META.get("REMOTE_ADDR", "0.0.0.0")Configure Redis as your cache backend for production (Memcached works too):
# settings.py
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.redis.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/1",
}
}With 5-minute cache TTL, a bot sending 1,000 requests/min from one IP generates just 1 API call instead of 60,000. That's a 99.998% reduction in API calls.
Step 3: View Decorators for Route-Specific Protection
Not every endpoint needs the same protection level. A login page needs strict checking; a public blog page doesn't. Django decorators let you apply different risk thresholds per view:
# botdetection/decorators.py
import functools
from django.http import JsonResponse
def require_low_risk(threshold=70, message="Access denied"):
"""
View decorator that blocks requests above the risk threshold.
Uses ip_risk data attached by BotDetectionMiddleware.
"""
def decorator(view_func):
@functools.wraps(view_func)
def wrapper(request, *args, **kwargs):
risk = getattr(request, 'ip_risk', {})
score = risk.get('risk_score', 0)
if score >= threshold:
return JsonResponse(
{"error": message, "risk_score": score},
status=403,
)
return view_func(request, *args, **kwargs)
return wrapper
return decorator
def require_human(message="Automated access not allowed"):
"""
Strict decorator: blocks VPNs, proxies, Tor, and datacenter IPs.
Use for sensitive endpoints like signup, checkout, password reset.
"""
def decorator(view_func):
@functools.wraps(view_func)
def wrapper(request, *args, **kwargs):
risk = getattr(request, 'ip_risk', {})
is_vpn = risk.get('is_vpn', False)
is_proxy = risk.get('is_proxy', False)
is_tor = risk.get('is_tor', False)
is_datacenter = risk.get('is_datacenter', False)
score = risk.get('risk_score', 0)
if score >= 80 or is_tor:
return JsonResponse(
{"error": message, "flags": {
"vpn": is_vpn, "proxy": is_proxy,
"tor": is_tor, "datacenter": is_datacenter,
}},
status=403,
)
return view_func(request, *args, **kwargs)
return wrapper
return decoratorNow apply them to your views:
# views.py
from botdetection.decorators import require_low_risk, require_human
# Strict: login and signup
@require_human()
def signup_view(request):
# Only clean IPs reach here
...
@require_low_risk(threshold=75)
def login_view(request):
# Moderately strict
...
# Relaxed: public content
def blog_view(request):
# No decorator — middleware still annotates request.ip_risk
# You can log it for analytics without blocking
...Step 4: Django REST Framework Permission Class
If you're building APIs with Django REST Framework, you can integrate bot detection as a permission class — DRF's native way to control access:
# botdetection/permissions.py
from rest_framework.permissions import BasePermission
class IsLowRiskIP(BasePermission):
"""
DRF permission: denies access if IP risk score exceeds threshold.
Requires BotDetectionMiddleware to be active.
"""
message = "Access denied due to suspicious IP activity."
def has_permission(self, request, view):
threshold = getattr(view, 'risk_threshold', 80)
risk = getattr(request, 'ip_risk', {})
return risk.get('risk_score', 0) < threshold
class IsNotBot(BasePermission):
"""
DRF permission: blocks known datacenter, proxy, and Tor IPs.
"""
message = "Automated access is not permitted on this endpoint."
def has_permission(self, request, view):
risk = getattr(request, 'ip_risk', {})
if risk.get('is_tor', False):
return False
if risk.get('is_datacenter', False) and risk.get('risk_score', 0) > 60:
return False
return risk.get('risk_score', 0) < 85Apply it to your API views:
# api/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from botdetection.permissions import IsLowRiskIP, IsNotBot
class SignupAPIView(APIView):
permission_classes = [IsNotBot]
risk_threshold = 70 # Custom threshold for this view
def post(self, request):
# Only clean traffic reaches here
return Response({"status": "account_created"})
class PublicDataAPIView(APIView):
permission_classes = [IsLowRiskIP]
risk_threshold = 90 # Lenient — only block the worst
def get(self, request):
return Response({"data": "public_info"})Or set it globally in DRF settings for a baseline:
# settings.py
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'botdetection.permissions.IsLowRiskIP',
],
}Step 5: Protect the Django Admin Panel
The Django admin panel (/admin/) is a prime target for credential stuffing and brute-force attacks. Add an extra middleware layer specifically for admin routes:
# botdetection/admin_protection.py
import logging
from django.http import JsonResponse
from django.conf import settings
logger = logging.getLogger("botdetection.admin")
class AdminBotProtectionMiddleware:
"""
Extra-strict bot protection for /admin/ routes.
Blocks VPNs, Tor, and high-risk IPs from admin login.
"""
ADMIN_PATHS = ("/admin/", "/admin/login/")
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
if not any(request.path.startswith(p) for p in self.ADMIN_PATHS):
return self.get_response(request)
risk = getattr(request, 'ip_risk', {})
ip = request.META.get("REMOTE_ADDR", "unknown")
# Strict: block VPN, Tor, datacenter, or score > 60
if (
risk.get('is_tor', False)
or risk.get('is_vpn', False)
or (risk.get('is_datacenter', False) and risk.get('risk_score', 0) > 40)
or risk.get('risk_score', 0) >= 60
):
logger.warning(
"Admin access blocked",
extra={
"ip": ip,
"risk_score": risk.get('risk_score'),
"is_vpn": risk.get('is_vpn'),
"is_tor": risk.get('is_tor'),
"path": request.path,
},
)
return JsonResponse(
{"error": "Admin access denied"},
status=403,
)
# Log all admin access attempts
logger.info(
"Admin access allowed",
extra={"ip": ip, "risk_score": risk.get('risk_score', 0)},
)
return self.get_response(request)# settings.py — add after BotDetectionMiddleware
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"botdetection.middleware.BotDetectionMiddleware",
"botdetection.admin_protection.AdminBotProtectionMiddleware",
# ... rest
]Step 6: Class-Based View Mixin
For Django's class-based views, a mixin provides clean integration:
# botdetection/mixins.py
from django.http import JsonResponse
class BotProtectionMixin:
"""
Mixin for class-based views. Set risk_threshold on the view
to control the blocking sensitivity.
"""
risk_threshold = 80
block_vpn = False
block_tor = True
block_datacenter = False
def dispatch(self, request, *args, **kwargs):
risk = getattr(request, 'ip_risk', {})
score = risk.get('risk_score', 0)
if score >= self.risk_threshold:
return self._block_response("risk_score_exceeded")
if self.block_tor and risk.get('is_tor', False):
return self._block_response("tor_exit_node")
if self.block_vpn and risk.get('is_vpn', False):
return self._block_response("vpn_detected")
if self.block_datacenter and risk.get('is_datacenter', False):
return self._block_response("datacenter_ip")
return super().dispatch(request, *args, **kwargs)
def _block_response(self, reason):
return JsonResponse(
{"error": "Access denied", "reason": reason},
status=403,
)
# Usage in views.py:
from django.views.generic import CreateView
from botdetection.mixins import BotProtectionMixin
from .models import Account
from .forms import SignupForm
class SignupView(BotProtectionMixin, CreateView):
model = Account
form_class = SignupForm
risk_threshold = 70
block_vpn = True
block_tor = True
block_datacenter = TrueStep 7: Async Risk Scoring with Celery
For high-traffic apps where you want to score IPs without adding latency to the request path, use Celery to process risk checks asynchronously:
# botdetection/tasks.py
import requests
from celery import shared_task
from django.core.cache import cache
from django.conf import settings
IPASIS_API_URL = "https://api.ipasis.com/v1/ip/{ip}"
CACHE_PREFIX = "ipasis_risk:"
CACHE_TTL = 300
@shared_task(bind=True, max_retries=2, default_retry_delay=5)
def score_ip_async(self, ip):
"""
Score an IP in the background. Result is cached for
subsequent requests from the same IP.
"""
cache_key = f"{CACHE_PREFIX}{ip}"
# Already cached?
if cache.get(cache_key):
return
try:
result = requests.get(
IPASIS_API_URL.format(ip=ip),
headers={
"Authorization": f"Bearer {settings.IPASIS_API_KEY}"
},
timeout=3,
)
data = result.json()
cache.set(cache_key, data, CACHE_TTL)
# If extremely high risk, add to blocklist
if data.get('risk_score', 0) >= 95:
cache.set(f"blocked:{ip}", True, 3600) # Block for 1 hour
except requests.RequestException as exc:
self.retry(exc=exc)Update the middleware to use the async path for low-priority routes:
# botdetection/middleware.py — async-aware version
from django.core.cache import cache
from django.http import JsonResponse
from botdetection.tasks import score_ip_async
CRITICAL_PATHS = ["/accounts/signup/", "/accounts/login/", "/api/v1/"]
class BotDetectionMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
ip = self.get_client_ip(request)
cache_key = f"ipasis_risk:{ip}"
# Check if IP is hard-blocked
if cache.get(f"blocked:{ip}"):
return JsonResponse(
{"error": "Blocked"}, status=403
)
data = cache.get(cache_key)
if data is None:
if self._is_critical_path(request.path):
# Synchronous check for critical paths
data = self._sync_check(ip)
else:
# Async check for non-critical paths
score_ip_async.delay(ip)
data = {}
request.ip_risk = data
if data.get('risk_score', 0) >= 90:
return JsonResponse(
{"error": "Request blocked"}, status=403
)
return self.get_response(request)
def _is_critical_path(self, path):
return any(path.startswith(p) for p in CRITICAL_PATHS)
def _sync_check(self, ip):
import requests as http_requests
from django.conf import settings as django_settings
cache_key = f"ipasis_risk:{ip}"
try:
resp = http_requests.get(
f"https://api.ipasis.com/v1/ip/{ip}",
headers={
"Authorization": f"Bearer {django_settings.IPASIS_API_KEY}"
},
timeout=2,
)
data = resp.json()
cache.set(cache_key, data, 300)
return data
except Exception:
return {}
@staticmethod
def get_client_ip(request):
xff = request.META.get("HTTP_X_FORWARDED_FOR")
if xff:
return xff.split(",")[0].strip()
return request.META.get("REMOTE_ADDR", "0.0.0.0")This gives you the best of both worlds: synchronous blocking for critical paths (login, signup, API) and non-blocking background scoring for everything else (blog pages, static content). First-time visitors to non-critical pages get zero added latency.
Step 8: Structured Security Logging
Good security requires visibility. Use Python's logging with structured output so your SIEM or log aggregator can parse bot detection events:
# botdetection/logging.py
import json
import logging
import time
logger = logging.getLogger("botdetection")
class SecurityEventLogger:
"""
Structured security event logger for bot detection.
Outputs JSON lines compatible with ELK, Datadog, etc.
"""
@staticmethod
def log_request(request, action="allow"):
risk = getattr(request, 'ip_risk', {})
ip = request.META.get("REMOTE_ADDR", "unknown")
event = {
"timestamp": time.time(),
"event": "bot_detection",
"action": action,
"ip": ip,
"path": request.path,
"method": request.method,
"risk_score": risk.get("risk_score", 0),
"is_vpn": risk.get("is_vpn", False),
"is_proxy": risk.get("is_proxy", False),
"is_tor": risk.get("is_tor", False),
"is_datacenter": risk.get("is_datacenter", False),
"country": risk.get("country_code", "unknown"),
"asn": risk.get("asn", "unknown"),
"isp": risk.get("isp", "unknown"),
"user_agent": request.META.get(
"HTTP_USER_AGENT", ""
)[:200],
}
if action == "block":
logger.warning(json.dumps(event))
else:
logger.info(json.dumps(event))Configure Django logging to capture these events:
# settings.py
LOGGING = {
"version": 1,
"handlers": {
"security_file": {
"class": "logging.FileHandler",
"filename": "/var/log/django/bot_detection.log",
"formatter": "json",
},
},
"formatters": {
"json": {
"format": "%(message)s", # Already JSON
},
},
"loggers": {
"botdetection": {
"handlers": ["security_file"],
"level": "INFO",
},
},
}Step 9: Complete Production Setup
Here's the complete file structure and settings for a production Django app with bot detection:
# Project structure
myproject/
├── botdetection/
│ ├── __init__.py
│ ├── middleware.py # Core middleware
│ ├── admin_protection.py # Admin-specific protection
│ ├── decorators.py # View decorators
│ ├── permissions.py # DRF permissions
│ ├── mixins.py # CBV mixin
│ ├── tasks.py # Celery async tasks
│ └── logging.py # Structured logging
├── myproject/
│ └── settings.py
└── manage.py# settings.py — complete bot detection config
IPASIS_API_KEY = env("IPASIS_API_KEY") # Use django-environ
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"botdetection.middleware.BotDetectionMiddleware",
"botdetection.admin_protection.AdminBotProtectionMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.redis.RedisCache",
"LOCATION": env("REDIS_URL", default="redis://127.0.0.1:6379/1"),
}
}
REST_FRAMEWORK = {
"DEFAULT_PERMISSION_CLASSES": [
"botdetection.permissions.IsLowRiskIP",
],
}
# Celery (for async scoring)
CELERY_BROKER_URL = env("REDIS_URL", default="redis://127.0.0.1:6379/0")
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {
"security_file": {
"class": "logging.handlers.RotatingFileHandler",
"filename": "/var/log/django/bot_detection.log",
"maxBytes": 10_000_000, # 10MB
"backupCount": 5,
"formatter": "json",
},
},
"formatters": {
"json": {"format": "%(message)s"},
},
"loggers": {
"botdetection": {
"handlers": ["security_file"],
"level": "INFO",
"propagate": False,
},
},
}Performance Benchmarks
We tested this setup on a Django 5.1 app running with Gunicorn (4 workers) behind Nginx on a 2-vCPU server:
| Scenario | Avg Latency | p99 Latency | API Calls/min |
|---|---|---|---|
| No bot detection | 12ms | 45ms | 0 |
| Sync, no cache | 38ms | 120ms | ~600 |
| Sync + Redis cache | 13ms | 52ms | ~10 |
| Async (Celery) + cache | 12ms | 46ms | ~10 |
With caching enabled, bot detection adds less than 1ms of overhead for cached IPs. The async + Celery approach is virtually zero-impact on the request path.
Bonus: Django-Specific Protection Patterns
Form Submission Protection
Protect Django forms from bot submissions by checking risk in form_valid:
# views.py
from django.views.generic import FormView
from django.contrib import messages
class ContactFormView(FormView):
template_name = "contact.html"
form_class = ContactForm
def form_valid(self, form):
risk = getattr(self.request, 'ip_risk', {})
if risk.get('risk_score', 0) >= 70:
messages.error(
self.request,
"Unable to process your request. "
"Please try again later."
)
return self.form_invalid(form)
# Process the form normally
form.save()
return super().form_valid(form)Django Signals for Real-Time Alerts
# botdetection/signals.py
from django.dispatch import Signal, receiver
from django.core.mail import mail_admins
# Custom signal
high_risk_detected = Signal() # sender, ip, risk_score, path
@receiver(high_risk_detected)
def notify_admins(sender, ip, risk_score, path, **kwargs):
if risk_score >= 95:
mail_admins(
subject=f"🚨 Critical risk IP: {ip} (score: {risk_score})",
message=(
f"A request from {ip} with risk score {risk_score} "
f"was blocked on {path}."
),
)
# In middleware, emit the signal:
from botdetection.signals import high_risk_detected
if risk_score >= 90:
high_risk_detected.send(
sender=self.__class__,
ip=ip,
risk_score=risk_score,
path=request.path,
)Gunicorn + Nginx Configuration
# nginx.conf — pass real IP to Django
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
}
}
# settings.py — trust nginx proxy headers
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
USE_X_FORWARDED_HOST = TrueConclusion
Django's middleware architecture makes it one of the easiest frameworks to add comprehensive bot detection to. With the patterns in this guide, you get:
- Global protection via middleware — every request is scored
- Granular control via decorators, DRF permissions, and CBV mixins
- Admin hardening — brute-force protection on
/admin/ - Zero-latency option — async Celery scoring for non-critical routes
- Production-ready caching — Redis-backed with Django's cache framework
- Visibility — structured logging for SIEM integration
The key insight: not every route needs the same protection level. Score globally, enforce locally. Let your login and signup endpoints be strict while your marketing pages stay frictionless.
Protect Your Django App Today
Get your IPASIS API key and start blocking bots in under 5 minutes.
Related Guides
Next.js Bot Detection Middleware
Add bot detection to Next.js with edge middleware and server components.
Integration GuideExpress.js Bot Detection Middleware
Build production-ready bot detection middleware for Express.js apps.
Deep DiveRedis Rate Limiting Patterns
Sliding window vs token bucket — which rate limiting algorithm fits your use case.
StrategyBot Detection Techniques for SaaS
Comprehensive overview of bot detection strategies for SaaS platforms.