Bot traffic is the invisible tax on Magento 2 performance. While human shoppers browse a few pages per session, bots crawl thousands of URLs, bypass caches, and strain your infrastructure — often without you knowing. A single misbehaving crawler can double your server load and spike your CDN bill.
This post covers everything you need to detect bot traffic, optimize for legitimate crawlers, and block the harmful ones. We'll look at practical Nginx, Varnish, and application-level solutions you can deploy today.
Why Bots Matter for Magento 2 Performance
Magento is a complex platform. Every uncached page requires:
- PHP-FPM workers to render templates
- MySQL queries for EAV product data
- Redis lookups for session and cache data
- Elasticsearch calls for layered navigation
A human visitor might hit 5–10 pages per session. A bot can crawl 50,000 pages in an hour. The difference is staggering.
Consider a store with 50,000 SKUs. A naive crawler following every product link, filter combination, and pagination page generates exponentially more requests than your actual customer base.
Typical bot impact:
- Cache hit ratio drops from 85% to 40% because bots request unique URLs
- MySQL connection pool exhaustion from concurrent
catalog_product_entitylookups - Elasticsearch cluster CPU spikes from facet aggregation queries
- CDN egress charges from uncached, CPU-rendered pages served to bots
If your analytics show "direct traffic" with a 100% bounce rate and 0s time-on-page, you might be looking at bot traffic disguised as humans.
Identifying Bot Traffic in Your Logs
Before you fix anything, you need to know what you're dealing with. Start with your access logs.
Nginx Access Log Analysis
# Top user agents by request count
awk -F '"' '{print $6}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20
# Requests per IP address
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20
# Bot-only traffic volume
awk '/Googlebot|bingbot|AhrefsBot|SemrushBot|MJ12bot/ {sum++} END {print sum}' /var/log/nginx/access.log
Magento Application-Level Detection
Log suspicious patterns from within Magento by extending the Http context or using an observer:
<?php
namespace Vendor\BotLogger\Observer;
use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;
use Magento\Framework\HTTP\PhpEnvironment\Request;
use Psr\Log\LoggerInterface;
class LogBotTraffic implements ObserverInterface
{
public function __construct(
private Request $request,
private LoggerInterface $logger
) {}
public function execute(Observer $observer): void
{
$userAgent = $this->request->getHeader('User-Agent') ?? '';
$ip = $this->request->getClientIp();
// Detect known bots
$bots = ['Googlebot', 'bingbot', 'AhrefsBot', 'SemrushBot', 'MJ12bot', 'DotBot'];
foreach ($bots as $bot) {
if (stripos($userAgent, $bot) !== false) {
$this->logger->info('BOT_TRAFFIC', [
'bot' => $bot,
'ip' => $ip,
'url' => $this->request->getRequestUri(),
'time' => microtime(true)
]);
break;
}
}
}
}
Track this in a separate log file and ingest it into your monitoring stack (Grafana, New Relic, or CloudWatch).
robots.txt — Your First Line of Defense
A well-crafted robots.txt is the simplest way to guide legitimate crawlers away from expensive pages.
Place robots.txt at pub/robots.txt (or root if your webroot is pub/):
User-agent: *
Disallow: /admin/
Disallow: /checkout/
Disallow: /customer/
Disallow: /catalogsearch/
Disallow: /sales/guest/
Disallow: /wishlist/
Disallow: /compare/
Disallow: /review/product/post/
Disallow: /newsletter/
Disallow: /sendfriend/
Disallow: /catalog/product_compare/
Disallow: /*?*price=
Disallow: /*?*color=
Disallow: /*?*size=
Disallow: /*?*material=
Disallow: /*?*limit=
Disallow: /*?*order=
Disallow: /*?*dir=
Disallow: /*?*mode=
# Allow CSS/JS/images for rendering
Allow: /pub/media/catalog/product/
Allow: /pub/static/
# Sitemap for efficient crawling
Sitemap: https://yourstore.com/sitemap.xml
# Crawl delay for all bots
Crawl-delay: 2
Key rules:
- Disallow layered navigation parameters — price, color, size, material filters generate thousands of unique URLs with identical content. These are SEO nightmares and performance drains.
- Disallow compare and wishlist — these are user-specific and should never be crawled.
- Set a crawl delay — 2 seconds between requests from a single bot is reasonable.
- Provide a sitemap — direct crawlers to your canonical URLs instead of letting them discover every filter combination.
Magento-Specific robots.txt Pitfalls
If your webroot is pub/ (recommended for security), robots.txt lives at pub/robots.txt. If your webroot is the Magento root, it goes there instead.
Don't rely solely on robots.txt for security — it's a polite request, not an enforcement mechanism. Malicious bots will ignore it entirely.
Nginx Rate Limiting for Bots
Rate limiting at the web server level is your second line of defense. It protects PHP-FPM and MySQL from being overwhelmed.
Zone-Based Rate Limiting
# In http context (nginx.conf)
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
limit_req_zone $bot_zone zone=bots:10m rate=1r/s;
# Map user agents to bot classification
map $http_user_agent $bot_zone {
default "";
~*(Googlebot|bingbot) $binary_remote_addr;
~*(AhrefsBot|SemrushBot) $binary_remote_addr;
~*(MJ12bot|DotBot|BLEXBot) $binary_remote_addr;
~*(Screaming|Majestic) $binary_remote_addr;
~*crawler $binary_remote_addr;
~*spider $binary_remote_addr;
}
# In server context (site config)
server {
# General limit for all traffic
limit_req zone=general burst=20 nodelay;
# Stricter limit for identified bots
location / {
if ($bot_zone != "") {
limit_req zone=bots burst=5 nodelay;
}
try_files $uri $uri/ /index.php$is_args$args;
}
# Always allow static assets (they're cached and cheap)
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2)$ {
limit_req off;
expires 1y;
add_header Cache-Control "public, immutable";
}
}
What this achieves:
- General traffic: 10 requests/second per IP with a burst of 20
- Bots: 1 request/second per IP with a burst of 5 — enough for legitimate crawlers, painful for aggressive ones
- Static assets: No rate limiting — images and CSS should be served freely from cache
Dynamic Bot Blocking with Nginx + Lua
For advanced setups, use OpenResty/Nginx with Lua to dynamically block bots based on behavior:
-- nginx.conf lua_shared_dict blocklist 10m;
init_worker_by_lua_block {
local blocklist = ngx.shared.blocklist
-- Preload known bad actors
blocklist:set("185.220.101.0/24", true, 86400)
}
access_by_lua_block {
local blocklist = ngx.shared.blocklist
local ip = ngx.var.remote_addr
if blocklist:get(ip) then
ngx.exit(ngx.HTTP_FORBIDDEN)
end
-- Block by excessive requests (detected via log analysis)
local req_count = blocklist:incr(ip .. ":count", 1, 0, 60)
if req_count > 100 then
blocklist:set(ip, true, 3600) -- Block for 1 hour
ngx.log(ngx.WARN, "Auto-blocked IP for excessive requests: ", ip)
ngx.exit(ngx.HTTP_TOO_MANY_REQUESTS)
end
}
Varnish Caching Strategy for Bots
Varnish can differentiate between humans and bots, applying different caching policies.
Bot-Aware VCL
sub vcl_recv {
# Normalize user agent for bot detection
if (req.http.User-Agent ~ "(Googlebot|bingbot|Slurp|DuckDuckBot|Baiduspider|YandexBot)") {
set req.http.X-Bot = "true";
} else {
set req.http.X-Bot = "false";
}
# Don't cache admin, checkout, customer area for anyone
if (req.url ~ "^/(admin|checkout|customer)/") {
return (pass);
}
# Cache product pages for bots with longer TTL (they don't need fresh prices instantly)
if (req.http.X-Bot == "true" && req.url ~ "^/catalog/product/view/") {
unset req.http.Cookie;
return (hash);
}
# For humans, respect cookies
if (req.http.Cookie) {
# Strip all but essential cookies
set req.http.Cookie = regsuball(req.http.Cookie, "(^|;)\s*__[a-z]+=[^;]*", "");
set req.http.Cookie = regsub(req.http.Cookie, "^;\s*", "");
if (req.http.Cookie == "") {
unset req.http.Cookie;
}
}
}
sub vcl_backend_response {
# Cache product pages for bots longer
if (bereq.http.X-Bot == "true" && bereq.url ~ "^/catalog/product/view/") {
set beresp.ttl = 1h;
set beresp.grace = 6h;
}
# Cache category pages for everyone
if (bereq.url ~ "^/catalog/category/view/") {
set beresp.ttl = 15m;
set beresp.grace = 1h;
}
}
sub vcl_deliver {
# Add debug header (remove in production)
set resp.http.X-Cache-Status = req.http.X-Bot;
}
Bot-specific caching benefits:
- Product pages: 1-hour TTL for bots — they're not price-sensitive in real-time
- Category pages: 15-minute TTL for everyone — bots get the same cached content as humans
- Grace period: Old cached content served while fetching fresh data prevents backend spikes
Blocking Malicious Bots
Not all bots are well-behaved. Some ignore robots.txt, spoof user agents, and aggressively crawl your site.
Nginx Bad Bot Blocker
Use the Nginx Bad Bot Blocker or implement a lightweight version:
# Block known bad user agents
if ($http_user_agent ~* (ahrefsbot|semrushbot|mj12bot|dotbot|blexbot|ezooms|yandexbot|baiduspider|sogou|exabot|facebot|facebookexternalhit)) {
# Allow search engines, block SEO tools and scrapers
if ($http_user_agent !~* (googlebot|bingbot|slurp|duckduckbot)) {
return 444; # Drop connection without response
}
}
# Block by IP reputation (maintain a deny list)
include /etc/nginx/bad_ips.conf;
Fail2Ban Integration
# /etc/fail2ban/filter.d/magento-bad-bot.conf
[Definition]
failregex = ^<HOST> .* "(GET|POST) .*" .* "(AhrefsBot|SemrushBot|MJ12bot|DotBot|BLEXBot)".*$
^<HOST> .* "(GET|POST) .*" .* HTTP/[0-9.]+" 4[0-9]{2} .* "(Python|curl|wget|Scrapy)".*$
ignoreregex =
# /etc/fail2ban/jail.local
[magento-bad-bot]
enabled = true
filter = magento-bad-bot
logpath = /var/log/nginx/access.log
maxretry = 10
findtime = 60
bantime = 3600
action = iptables-multiport[name=BadBots, port="http,https", protocol=tcp]
Magento 2 Firewall Module
For application-level blocking, use a module like magento2-firewall or implement your own:
<?php
namespace Vendor\BotBlocker\Plugin;
use Magento\Framework\App\FrontControllerInterface;
use Magento\Framework\App\RequestInterface;
use Magento\Framework\HTTP\PhpEnvironment\Response;
class BotBlocker
{
private array $blockedAgents = [
'AhrefsBot', 'SemrushBot', 'MJ12bot', 'DotBot',
'BLEXBot', 'Ezooms', 'curl', 'wget', 'python-requests'
];
public function beforeDispatch(
FrontControllerInterface $subject,
RequestInterface $request
): void {
$userAgent = $request->getHeader('User-Agent') ?? '';
foreach ($this->blockedAgents as $bot) {
if (stripos($userAgent, $bot) !== false) {
$response = new Response();
$response->setHttpResponseCode(403);
$response->setContent('Forbidden');
$response->sendResponse();
exit;
}
}
}
}
Googlebot Crawl Budget Optimization
Googlebot has a limited crawl budget for your site. Make sure it spends that budget on pages that matter.
1. Consolidate Pagination with rel="canonical"
<!-- On page 2 of category listing -->
<link rel="canonical" href="https://yourstore.com/electronics.html" />
<link rel="prev" href="https://yourstore.com/electronics.html" />
<link rel="next" href="https://yourstore.com/electronics.html?p=3" />
2. Noindex Low-Value Filter Pages
<!-- On layered navigation filter page with no products -->
<meta name="robots" content="noindex, follow" />
3. Proper Sitemap Management
<?php
// In your sitemap generation cron or module
namespace Vendor\Sitemap\Model;
use Magento\Sitemap\Model\Sitemap;
class OptimizedSitemap extends Sitemap
{
protected function _initSitemapItems()
{
parent::_initSitemapItems();
// Exclude out-of-stock products from sitemap
foreach ($this->_sitemapItems as $key => $item) {
if ($item->getUrl() && $this->isOutOfStockProduct($item->getUrl())) {
unset($this->_sitemapItems[$key]);
}
}
// Limit to 50,000 URLs per sitemap (Google's limit)
$this->_sitemapItems = array_slice($this->_sitemapItems, 0, 50000);
}
}
4. Monitor Google Search Console
Check the "Crawl Stats" report weekly. Look for:
- Crawl rate drops — Google might be hitting errors or timeouts
- Crawl budget waste — pages that return 404 or are noindex being crawled repeatedly
- Server error spikes — usually means bots are hitting broken URLs or overloading your infrastructure
Measuring Bot Impact
Track these metrics before and after implementing bot controls:
| Metric | Before | After |
|---|---|---|
| Cache hit ratio | 40–60% | 75–85% |
| Average response time | 800ms | 300ms |
| PHP-FPM queue depth | 50+ | <10 |
| MySQL queries/sec | 5000+ | <2000 |
| CDN egress (GB/day) | 500 | 150 |
| Bot requests/hour | 50,000 | <5,000 (legitimate only) |
Quick Nginx Log Dashboard
#!/bin/bash
# bot-impact.sh — run this hourly via cron
LOG=/var/log/nginx/access.log
HOUR=$(date -d '1 hour ago' +%H)
# Total requests in the last hour
TOTAL=$(awk -v h="$HOUR" '$4 ~ ":"h {count++} END {print count+0}' "$LOG")
# Bot requests
BOTS=$(awk -v h="$HOUR" '$4 ~ ":"h && /(Googlebot|bingbot|AhrefsBot|SemrushBot|MJ12bot)/ {count++} END {print count+0}' "$LOG")
# Cache misses (adjust based on your setup)
MISSES=$(awk -v h="$HOUR" '$4 ~ ":"h && /MISS/ {count++} END {print count+0}' "$LOG")
PCT=0
if [ "$TOTAL" -gt 0 ]; then
PCT=$((BOTS * 100 / TOTAL))
fi
echo "$(date): Total=$TOTAL, Bots=$BOTS (${PCT}%), Cache misses=$MISSES"
Summary Checklist
- [ ] Implement a strict
robots.txtwith layered navigation exclusions - [ ] Set up Nginx rate limiting for both general traffic and bots
- [ ] Configure Varnish with bot-aware caching policies
- [ ] Deploy Fail2Ban to auto-block aggressive bots
- [ ] Add application-level bot logging and blocking
- [ ] Optimize sitemap.xml to guide crawlers efficiently
- [ ] Monitor Google Search Console crawl stats weekly
- [ ] Track cache hit ratio, response time, and server load metrics
Final Thoughts
Bot traffic is not going away. If anything, AI scrapers and SEO tools are becoming more aggressive. The good news is that most bot-related performance issues are solvable with the right combination of robots.txt guidance, rate limiting, and caching strategy.
The key is to be proactive. Don't wait for your server to crash during a crawler rampage. Implement these controls now, monitor your logs weekly, and adjust your defenses as bot behavior evolves. Your cache hit ratio, server costs, and sanity will thank you.
Top comments (0)