Problem/Motivation
Last night, a massive bot horde arrived, and the server was timing out. So this morning I looked around for additional measures, and found Cookie Bot Protection, which works really great with Crawler Rate Limit.
Out of curiosity I checked the HTTP response status codes. Luckily, the two solutions use different codes: Crawler Rate Limit uses 429 and Cookie Bot Protection uses 302 or 401, so they can be easily distinguished.
I added Cookie Bot Protection 11.49 today, and the changes can be seen in the log excerpts below.
As can be seen, Cookie Bot Protection takes over as the most active blocker, maybe simply because "Co" comes before "Cr" in the alphabet?
Since Crawler Rate Limit has a strong focus on low resource usage, I believe it would be ideal, if it was the first line of defence, and Cookie Bot Protection could handle those bots that get through.
I included "Served pages -- 200" to show how efficient the two modules are in combination, accepted requests dropped from ~25% to 1-2%.
Steps to reproduce
Use Crawler Rate Limit with Cookie Bot Protection, and see that Cookie Bot Protection seem to execute first.
Proposed resolution
We could raise the http middleware value to 300, to make sure Crawler Rate Limit is placed first in the order of execution, making it the first defence layer blocking with a 429 Too Many Requests response. Cookie Bot Protection can handle those bots that get through, via cookie inspection.
Remaining tasks
User interface changes
API changes
Data model changes
Log excerpts
Crawler Rate Limit -- 429
$ for i in $(seq -f "%02g" 0 23); do printf "%02d" ${i#0}; grep "/2026:$i:" /var/log/apache2/access.log | awk '{if ($9 == 429) {limited++} else {served++}} END { percent = NR ? (limited/NR)*100 : 0; printf(" %6d %6d %6d %6d%%\n", NR, served, limited, percent) }'; done # per hour
00 67744 36810 30934 45%
01 68666 36387 32279 47%
02 69533 36399 33134 47%
03 68887 36768 32119 46%
04 67813 35607 32206 47%
05 68561 34977 33584 48%
06 67208 34558 32650 48%
07 69486 33771 35715 51%
08 91217 39016 52201 57%
09 88172 38798 49374 55%
10 90250 39206 51044 56%
11 110584 64810 45774 41%
12 240202 214850 25352 10%
13 242732 217273 25459 10%
14 236387 212920 23467 9%
15 246129 220925 25204 10%
16 248577 223127 25450 10%
17 254610 227044 27566 10%
18 229108 202590 26518 11%
19 229850 203144 26706 11%
20 167366 148426 18940 11%
21 0 0 0 0%
22 0 0 0 0%
23 0 0 0 0%Cookie Bot Protection -- 302 or 401
$ for i in $(seq -f "%02g" 0 23); do printf "%02d" ${i#0}; grep "/2026:$i:" /var/log/apache2/access.log | awk '{if ($9 == 302 || $9 == 401) {limited++} else {served++}} END { percent = NR ? (limited/NR)*100 : 0; printf(" %6d %6d %6d %6d%%\n", NR, served, limited, percent) }'; done # per hour
00 67744 67691 53 0%
01 68666 68663 3 0%
02 69533 69533 0 0%
03 68887 68810 77 0%
04 67813 67811 2 0%
05 68561 68559 2 0%
06 67208 67204 4 0%
07 69486 69485 1 0%
08 91217 91198 19 0%
09 88172 88170 2 0%
10 90250 90226 24 0%
11 110584 81499 29085 26%
12 240202 46486 193716 80%
13 242732 46612 196120 80%
14 236387 43807 192580 81%
15 246129 46206 199923 81%
16 248577 48067 200510 80%
17 254610 50998 203612 79%
18 229108 50676 178432 77%
19 229850 50816 179034 77%
20 171361 38461 132900 77%
21 0 0 0 0%
22 0 0 0 0%
23 0 0 0 0%
Served pages -- 200
$ for i in $(seq -f "%02g" 0 23); do printf "%02d" ${i#0}; grep "/2026:$i:" /var/log/apache2/access.log | awk '{if ($9 == 200) {limited++} else {served++}} END { percent = NR ? (limited/NR)*100 : 0; printf(" %6d %6d %6d %6d%%\n", NR, served, limited, percent) }'; done # per hour
00 67744 51922 15822 23%
01 68666 53165 15501 22%
02 69533 53565 15968 22%
03 68887 52471 16416 23%
04 67813 51970 15843 23%
05 68561 53116 15445 22%
06 67208 51886 15322 22%
07 69486 48605 20881 30%
08 91217 67091 24126 26%
09 88172 63374 24798 28%
10 90250 64532 25718 28%
11 110584 88549 22035 19%
12 240202 234700 5502 2%
13 242732 237154 5578 2%
14 236387 231121 5266 2%
15 246129 241172 4957 2%
16 248577 243826 4751 1%
17 254610 249714 4896 1%
18 229108 223288 5820 2%
19 229850 223825 6025 2%
20 173031 168431 4600 2%
21 0 0 0 0%
22 0 0 0 0%
23 0 0 0 0%Issue fork crawler_rate_limit-3614314
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 #2
vaish commentedI just checked the source code of Cookie Bot Protection. It's implemented as http middleware, just like Crawler Rate Limit. Module weight has no effect on the order of execution in this case. Order is determined by the priority configured for the http middleware service in the module's services.yml file. Both modules are setting the priority to be higher than page cache. CRL's priority is set to 240, while Cookie Bot Protection chose to set it to 250. Hence, it has higher priority than CRL.
Crawler Rate Limit
Cookie Bot Protection
Comment #4
ressaGreat digging, thanks for reporting back. I found the corresponding Drupal core file core/lib/Drupal/Core/DependencyInjection/Compiler/StackedKernelPass.php, and the comment says:
... so I set
http_middleware, priorityto 300 in the MR. Do you that could work? After all, since we want Crawler Rate Limit to run before anything else, it is the definition of a high priority middleware.Comment #5
ressaI had a chance to benchmark the MR. It makes a big positive difference and the server load is reduced ~50%: As first line of defence, Crawler Rate Limit does a lot of checking very quickly, while Cookie Bot Protection makes a more thorough cookie-based inspection of the bots that slip through.
Also, the total number of requests per minute dropped ~65% from ~4,700 to ~1,700 per minute, probably due to the
429 Too Many Requestsresponse from Crawler Rate Limit, which makes some bots back off. Cookie Bot Protection probably does much "repeat blocking" of the same bot, due to its more accepting302or401responses, making bots behave differently and maybe try again right away?From https://apistatuscheck.com/blog/api-error-codes-cheat-sheet
Before and after from the server logs, per hour:
Crawler Rate Limit -
429Cookie Bot Protection --
302or401Before and after from the server logs, per minute:
Crawler Rate Limit -
429Cookie Bot Protection --
302or401Comment #6
vaish commentedThanks for testing the MR and providing detailed feedback. Do you have any new findings since your last report? Regarding your MR, I would reduce the priority to make sure it remains lower than reverse proxy middleware which is also set at 300. For CRL, priority of 260 should be enough. That's a minimal change we can make to ensure CRL runs before Cookie Bot Protection.
Comment #7
ressaThanks for the suggestion, I lowered the priority as you suggested. The patch has worked well since I applied it, and there is calm on the server. Most bots are blocked via CRL ASN blocking (403), followed by CRL standard time limit (429), and Cookie Bot Protection is the last check, as can be seen below. It's fairly calm right now.
Two weeks ago -- Cookie Bot handled 2/3 of visits
Cookie Bot Protection --
302or401Crawler Rate Limit -
403or429Today, Cookie Bot handles ~5%
Cookie Bot Protection --
302or401Crawler Rate Limit -
403or429Comment #9
vaish commentedMerged. Thank you, @ressa.
Something to keep in mind while generating your reports. HTTP response codes 403 and 302 are quite common in Drupal. Some of the requests you are counting towards CRL or Cookie Bot Protection may actually be unrelated. E.g. 403 could be caused by anonymous user attempting to access Drupal's admin interface.
Here is relevant section from CRL's README:
Comment #11
ressaPerfect, thank you @vaish!
And thanks for clarifying that many of those 403's could be from elsewhere. I had a look in the log files for 403's, and the recent ones all seemed to be around 4K (
[...] HTTP/1.1" 403 4191 "-" "Mozilla [...]) so they are probably from a hard coded user agent block in .htaccess I added recently.In older log files (before the .htaccess user agent block) I see many inserts like
[...] HTTP/1.1" 403 259 "-" "Mozilla [...], I guess those could be from Crawler Rate Limit or Cookie Bot Protection.