How to Block Bots on LiteSpeed: Four Fixes, and Why Only One Worked

Date:

A companion to the diagnosis story. Once we knew a single SEO crawler had drained a month’s CDN budget in three days, stopping it looked trivial: flip a toggle, edit a file. It took four attempts — and the three that failed all failed for the same reason. Not effort. Position.

Having proven the culprit — one reputable SEO crawler walking 127,000 product URLs from a rotating pool of datacenter proxies — the fix seemed like a five-minute job. It was not. The obvious levers each did nothing, and each taught the same lesson: a blocker only stops what actually reaches it, and every cache or proxy in front of it is a blind spot.

A request path is the ordered set of layers a request crosses — CDN edge, web server, page cache, PHP, application — before a page comes back. Where you place a block on that path decides whether it ever sees the traffic you are trying to stop. We placed it wrong three times.

The short answer: to block bots on LiteSpeed reliably, put the rule at the outermost layer every request crosses — the web server (.htaccess/LiteSpeed), which runs before the page cache. Three of our four attempts did nothing, and the difference was never effort; it was position. A block placed behind the page cache (a security plugin in PHP) or behind a bypassed CDN edge never sees the cached hits it is meant to catch, so it silently returns 200. The .htaccess user-agent rule worked because it sits in front of the cache, across every rotating IP. And match the tool to the bot: name-based rules only catch crawlers that announce themselves — a scraper spoofing a browser needs IP, rate, or challenge measures.

Where to block bots on LiteSpeed: the request path

Where each blocker sits — and why position decided the outcomeThe request path, outermost layer to deepest. Red = failed or blind.
robots.txt

The bot reads it first. Honest bots only, and only next crawl cycle.

CDN edge

Under Attack Mode challenges HERE.

x bypassed — edge out of path
.htaccess 403

Web-server door, BEFORE the cache. Catches every IP. This is the fix.

Page cache

Serves cached pages before PHP even runs.

x blind spot below this line
Wordfence (PHP)

UA + rate block — but never sees a cached hit.

x too deep — returned 200

A blocker only stops what reaches it. Every cache in front of it is a blind spot — so enforce at the outermost layer the traffic actually passes through.

Read it left to right — that is the order a request crosses the layers. Three are red: they failed or were blind. The one that worked sits at exactly the point every request must pass, in front of the cache.

Attempt 1 — The CDN toggle

Expectation: the CDN has an “Under Attack Mode” — a one-click challenge that makes every visitor pass a lightweight check before being served. Flip it, bots stop.

Redacted CDN status panel reading "CDN Bypassed — Exceeds bandwidth limit", with an Under Attack Mode toggle in the Quick Actions panel.

Reality: nothing changed. The clue was on the same panel — “CDN Bypassed — exceeds bandwidth limit.” Having blown its quota, the CDN had taken itself out of the path; traffic was falling straight through to the origin. Under Attack Mode is an edge feature, and the edge was no longer serving the site. A challenge at a layer that is not in the request path challenges nobody. The toggle only helps once the quota resets and the edge is fronting traffic again.

An edge feature cannot act on traffic the edge is no longer serving.

Attempt 2 — robots.txt

Expectation: add Disallow: / for the crawler and it leaves.

Reality: three surprises. First, there was no robots.txt in the web root to edit — WordPress serves a virtual one, generated on the fly, so the file only exists at the URL, not on disk. Second, once edited, it changed nothing immediately: robots.txt is a request honest bots re-read on their own schedule — hours, not seconds — and it does nothing to bots that ignore it. Third, a subtle foot-gun: dropping the crawler rules above the existing User-agent: * block orphaned the store’s WooCommerce rules, briefly leaving normal search engines unrestricted. robots.txt is a request that compliant crawlers re-read on their own schedule, never an enforced block — the polite layer: worth setting, but slow, optional, and honor-system.

Attempt 3 — The security plugin

Expectation: the site runs a well-known security plugin with a firewall. Block the crawler’s user-agent there and it dies at the door.

Redacted security-plugin live-traffic log: row after row of one SEO crawler from a Western-European hub, every one to three seconds, all resolving to the same proxy network, all returning 200.

Reality: the block was real, and it still returned 200. The plugin runs in PHP — and on LiteSpeed the page cache serves a cached page before PHP ever loads. With an aggressive cache TTL, essentially the whole catalog was cached, so the crawler’s hits were answered by the cache and never reached the plugin. We proved it with two requests:

# cached homepage — the plugin never runs
curl -sI -A "…AhrefsBot…" https://example-store/    →  200   X-LiteSpeed-Cache: hit

# a cache-miss URL — now PHP runs, and the plugin DOES block
curl -sI -A "…AhrefsBot…" "https://example-store/?bust=1739"  →  503  (block page)
Code language: Bash (bash)

The plugin works — but only on cache misses. It is the same blind-spot as the CDN, one layer deeper: a cache in front of your blocker hides the very traffic you are hunting. (A per-IP rate limit had the same problem for a different reason — the crawler spread its load across dozens of proxy IPs, so no single-IP counter ever tripped.)

A cache in front of your blocker hides the very traffic you are hunting.

Attempt 4 — The .htaccess rule (the fix)

Expectation: by now, low. It was the one that worked.

The web server (LiteSpeed here, Apache elsewhere) evaluates .htaccess before it serves the page cache. A mod_rewrite user-agent rule there returns 403 at the door — in front of the cache, before PHP, across every rotating IP, cached URL or not:

<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTP_USER_AGENT} (AhrefsBot|SemrushBot|GPTBot|…) [NC]
RewriteRule .* - [F,L]
</IfModule>
Code language: Apache (apache)

We verified it was genuinely the .htaccess doing the work — not the plugin block still lingering — by testing user-agents that were only in the .htaccess list, never in the plugin:

curl -sI -A "SemrushBot"  https://example-store/  →  403   (787 B)   # only in .htaccess
curl -sI -A "GPTBot"      https://example-store/  →  403   (787 B)   # only in .htaccess
curl -sI -A "Googlebot"   https://example-store/  →  200             # correctly spared
Code language: Bash (bash)

The 403 (a small web-server page) is a different fingerprint from the plugin’s 503 (a large block page) — proof it was intercepted at the web-server layer, before anything deeper ran. (The status code you serve is a signal in its own right: a maintenance page, for instance, should answer 503, not 200, or crawlers treat the placeholder as real content.)

The expectation-versus-reality ledger

AttemptExpectationRealityRoot cause
CDN “under attack” toggleInstant challenge stops botsNo effectEdge was bypassed — out of the request path
robots.txt disallowCrawler leavesSlow, honor-system, no file to editA request, read next crawl cycle; virtual file
Security-plugin UA blockDies at the door200 on cached pagesPlugin is in PHP — behind the page cache
.htaccess UA 403(Low)WorkedWeb-server layer — in front of the cache

Same lesson four times: the block has to live at the outermost layer the traffic actually crosses.

Enforce at the outermost layer the traffic crosses, or enforce nothing at all.

The part nobody expected: the swarm behind the swarm

Blocking the top crawler surfaced a second problem. Watching live traffic afterwards, a mix of other bots filled the gap — and only some were nameable (other SEO and AI crawlers, added to the same list; the reputable ones even publish their bot tokens so you can target them). A large share were datacenter scrapers spoofing an ordinary browser user-agent — one announced itself as a Chrome version that does not exist, from a hosting provider’s IP. No user-agent rule can catch a bot that lies about its user-agent. That traffic needs a different toolset entirely: per-IP rate limits (partial, against rotating proxies), datacenter/ASN or hostname blocks, or a CDN challenge once the edge is back in the path. User-agent blocking cleans up the honest bots — the same tactic that hides an email address from harvesting bots works on the crawlers that identify themselves; it just has a hard ceiling at the dishonest ones.

A name-based rule cannot stop a bot that lies about its name.

The short version

Blocking bots on LiteSpeed is a placement problem, not an effort problem. Put the rule at the outermost layer every request crosses — the web server (.htaccess/LiteSpeed), in front of the page cache — because anything ahead of it (a bypassed CDN edge, a full-page cache) can answer a request before the rule ever runs. That is why robots.txt, the CDN toggle, and the in-PHP security plugin all missed, and the .htaccess 403 did not. And when a bot spoofs its user-agent, stop reaching for name-based rules: switch to IP, rate, or challenge measures that judge behavior instead of a label the bot controls.

Frequently Asked Questions

Why didn’t my security plugin block the crawler even though I added the rule?

Almost certainly because a page cache sits in front of it. Plugins like Wordfence run in PHP, and a full-page cache (LiteSpeed, Varnish, a CDN) serves a cached page before PHP loads — so the plugin never sees a cached request and can’t block it. It will still catch cache misses. To block cached hits, move the rule to the web-server layer (.htaccess/LiteSpeed) or exclude the bot’s user-agent from caching.

Where is my robots.txt if there’s no file in the web root?

WordPress serves a virtual robots.txt — generated dynamically, existing only at the /robots.txt URL, not on disk. Edit it through your SEO plugin, or drop a real file in the web root to override the virtual one. Either way, remember robots.txt is a request compliant bots honor on their own schedule, not an enforced block.

Why doesn’t the CDN’s “under attack” mode stop the bots?

If your CDN shows “bypassed” or “over quota”, it has taken itself out of the request path — traffic is going straight to your origin, and an edge-level challenge can’t run on traffic the edge isn’t serving. The toggle only works while the CDN is actively fronting your site.

Will blocking user-agents stop all the bots?

No — only the ones that announce themselves. Datacenter scrapers that spoof a normal browser user-agent are invisible to name-based rules. For those you need per-IP rate limiting, hostname or ASN blocks, or a challenge/bot-management layer that judges behavior rather than the user-agent string.

Is an .htaccess block safe on a live site?

It is powerful and therefore worth testing: a user-agent pattern that is too broad can 403 real visitors or the search engines you want. Keep the list to specific bot tokens, exclude the crawlers you value (Googlebot, Bingbot, your social unfurlers), and verify with curl -A right after — a bad bot should get 403 while a normal browser and the major search bots still get 200.

Share post:

Subscribe

Popular

More like this
Related

How to Merge Spreadsheets by URL (and Why Our Tool Reported 5 Misses That Weren’t)

Matching two spreadsheets on a URL column is the same job as matching a stock list to a price list by product code. It goes wrong in the same two places: how you clean the code, and how you count what didn't match.

Many-to-One: Why a Mapping Backlog Outgrows Its Target Taxonomy

You're mapping a messy source vocabulary onto a clean target taxonomy. The target has about 5,600 slots, so the job must be roughly 5,600 decisions — right? The backlog is already past 8,900, and still climbing.

The Fix That Didn’t Take: When a Content-Hash Gate Skips Your Code Change

You shipped a fix to how one field is computed, re-ran the pipeline across everything, and it reported a clean run. Then the consistency check still flags those records as wrong. The fix is in the code, the rebuild "ran" — and the records never changed. Your own optimisation skipped them.

Your Dedup Scan Is Over-Counting — Because It Forgot What You Already Fixed

The report said 80 duplicate groups still to resolve. You'd been resolving them for days. You run it again after a big batch and it *still* says 80-ish. The backlog isn't stubborn — the scan is counting work you already finished.