Problem/Motivation
I have recently tried the ASN-based bot-blocking feature, and it works extremely well.
It's exceptionally efficient at blocking non self-identifying, or even cloaked bots, with spoofed user agent strings (see below), trying to pass as a regular visitor. Sadly, the "Limit for crawler / bot traffic (visitors that openly identify as crawlers / bots)" (bot_traffic) method has become less and less useful, due to these bad actors ...
I started with this setting:
$settings['crawler_rate_limit.settings']['regular_traffic_asn'] = [
'interval' => 30,
'requests' => 30,
... and when I looked in the logs, it seemed that somewhere in the process, some IPs were not included under an ASN in the database, so that IPs from the same ASN were both getting blocked ("429") and not ("200") at the same time. The ratio was roughly that 75% were blocked while 25% slipped through ...
Example (notice the spoofed user agent):
429
145.223.132.137 - - [16/Apr/2026:10:36:49 +0200] "GET /removed-path HTTP/1.1" 429 2850 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/184.1"
200
145.223.132.140 - - [16/Apr/2026:10:36:49 +0200] "GET /removed-path HTTP/1.1" 200 91430 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"But when I took a closer look, it turned out that the erroneous 200's happened every 30. second, matching the interval, which makes sense: In effect, the rules needed re-training at that moment and the bots needed to hit the ASN limit.
I then tried tried this:
'interval' => 600,
'requests' => 600,... which worked well, except that every tenth minute, 600 bot visits were needed for the block to kick in, spiking Load Average to 10 (see "Log data excerpt" below). Tightening up interval/requests ratio to this made all the difference:
'interval' => 600,
'requests' => 60,Load Average is now around a healthy ~0.65, and CPU went down from ~40% to ~8%, due to the serve/block ratio now hitting numbers like 81/384 (i.e. 20% served, 80% blocked), resulting in much less strain on the server:

So perhaps the interval/requests values for the ASN example in the README can be tightened up, to make this method more efficient from the get go?
Steps to reproduce
Proposed resolution
Set the interval/requests values for the ASN example in the README to this?
'interval' => 600,
'requests' => 60,
Remaining tasks
User interface changes
API changes
Data model changes
Log data excerpt
11:00 - 11:45
# for i in $(seq -f "%02g" 0 59); do printf "%02d" ${i#0}; grep "/2026:11:$i:" /var/log/apache2/access.log | awk '{if ($9 == 429) {limited++} else {served++}} END { printf("%5d %5d %5d\n", NR, served, limited) }'; done
Min Total 200 429 <<< columns
00 582 420 162 START 30/30 (interval/requests)
01 605 437 168 miss rate ~25%
02 579 386 193
03 582 415 167
04 561 417 144
05 539 353 186
06 600 417 183
07 563 459 104
08 703 703 0 START 600/600 (interval/requests)
09 701 701 0 10 minute spikes - Load Average (15 min) = ~4 / CPU ~40%
10 659 602 57
11 706 495 211
12 681 493 188
13 664 422 242
14 1149 932 217
15 556 341 215
16 567 350 217
17 598 385 213
18 545 545 0
19 585 585 0
20 555 537 18
21 520 317 203
22 427 278 149
23 483 285 198
24 513 312 201
25 560 356 204
26 522 277 245
27 534 263 271 START 600/60 (interval/requests)
28 539 297 242 Load Average (15 min) = ~0.65 / CPU ~8%
29 537 163 374
30 408 101 307
31 452 100 352
32 474 112 362
33 462 120 342
34 412 63 349
35 433 68 365
36 397 57 340
37 410 142 268
38 633 406 227
39 443 78 365
40 570 231 339
41 400 83 317
42 389 73 316
43 465 81 384
44 484 163 321
45 438 125 313| Comment | File | Size | Author |
|---|---|---|---|
| Crawler-Rate-Limit-ASN-efficiency.png | 126.83 KB | ressa |
Issue fork crawler_rate_limit-3585204
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #3
ressaComment #4
ressaThe serve:block ratio has switched from about 2.5:1 to 1:2.5, very nice:
Comment #5
ressaComment #6
vaish commented@ressa thanks for the detailed post. It nicely explains how to use server logs to arrive at the rate limiter settings that make sense for your particular case. It's impressive to see the reduction in server load and bandwidth you were able to achieve.
My concern is that ASN based rate limiting may affect genuine traffic if set too low. Single ASN covers many IP addresses and for all of those together you are allowing only 60 requests per 10 minutes. What if ASN belongs to a popular ISP in your area? All the visitors using that ISP will be counted towards the single low limit and they may end up being rate limited. Did you consider that possibility?
I'm also curious what's your setting for regular traffic? It would make sense to have lower limit for regular traffic (IP address + user agent) then for ASN. Otherwise, you will always hit ASN limit before regular traffic limit has been reached.
That being said, I agree that numbers used in the README should be tweaked. Unfortunately, there is no way to provide numbers that will work for everyone. Numbers you want to use will depend on the amount of traffic your server receives and on the server's capacity to handle it.
Comment #7
ressaYou're welcome @vaish, thanks for your thorough reply as always. Showcasing all the helpful log example commands from the README was a nice side effect :)
You're right, those settings do risk blocking genuine traffic. Though in my case, the number of visits are not high enough, so there is no danger of grouping several real visits under a single ASN, blocking real visitors.
These are my settings:
Thinking more about this, I do agree with you that the example settings will not work for everyone, they need to be set on a case by case basis ...
And actually, about this issue -- my only problem with the 600/600 setting for ASN is the recurring spikes every ten minutes, where the "quota" is reset to zero and needs to hit 600 visits for an ASN, before 429's kick in again, which puts the CPU under strain.
So instead of tweaking the numbers, 600/600 could actually work fine, if only there was a more gradual transition, and a fraction of the older entries were deleted at some interval. Maybe it's possible to introduce a First-In-First-Out (FIFO) queue, where older registrations are dropped gradually?
Something like this:
This way, 90% of the counted IP's are retained, and the interval for comparison slowly slides "one minute ahead", step by step. Do you think something like that is possible, and worth considering? Please ask, if my request doesn't make sense :)
Comment #8
vaish commentedYour request makes sense. However, it's out of the scope of Crawler Rate Limit module. Actual rate limiting is performed by third party library nikolaposa/rate-limit. Changes you are proposing would need to be implemented in that library, not in CRL module.
This issue should focus on updating settings example in README file.
Comment #9
ressaI understand, and thanks for the feedback and tips on where to suggest this feature. I created Drop older registrations gradually via First-In-First-Out (FIFO) queue #59, as well as an issue here, postponed until or if it gets added, #3590091: Drop older ASN registrations gradually via First-In-First-Out (FIFO) queue.
Comment #11
vaish commentedMerged. I tweaked both regular traffic and asn values. Thanks @ressa.
Comment #13
ressaThanks @vaish, those updates and values look good.