


















    
        
    

    
        

        
            
        
    
        

        
            
        
    




<?xml version="1.0" encoding="utf-8"?>

<rss version="2.0"  xml:lang="en-sv"  xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        

        

        
            <language>en-sv</language>
        

        
            <lastBuildDate>Tue, 11 Aug 2026 12:00:00 CEST</lastBuildDate>
        

        
            <link>https://blog.troed.se/tags/bot-mitigation/</link>
        

        
            <atom:link href="https://blog.troed.se/tags/bot-mitigation/rss.xml" hreflang="en-sv" rel="self" type="application/rss+xml"/>
        

        
            

            <atom:link href="https://blog.troed.se/tags/bot-mitigation/" hreflang="en-sv" rel="alternate" type="text/html"/>
        
            

            <atom:link href="https://blog.troed.se/tags/bot-mitigation/rss.xml" hreflang="en-sv" rel="alternate" type="application/rss+xml"/>
        

        

        

        
            <title>Bot-Mitigation · Tags · Things I couldn&rsquo;t find elsewhere</title>
        

        

        
            
                <item>
                    
                    
                    
                    
                    
                    
                    

                    

                    

                    

                    

                    
                    
                    

                    

                    
                        <description><![CDATA[<p>Recently I noticed unusually high load on one of my servers. It turned out to be due to my somewhat new Forgejo service being absolutely hammered by bots. It wasn&rsquo;t a typical brute-force attack, but rather a massive, distributed crawl from the Meta (Facebook) crawler. I guess they want all that juicy code we produce.</p><p>The logs were a mess. While no single IP was doing anything egregious (most were just doing 1-3 requests per minute), the sheer volume was overwhelming. We were seeing hundreds of thousands of hits a day, all from the same broad Meta range. A quick look at the User-Agent showed <code>meta-externalagent/1.1</code> everywhere.</p><p>The problem with traditional per-IP rate limiting is that these crawlers are distributed. If you set a limit of, say, 10 requests per second per IP, a botnet of 1,000 IPs can still hit you with 10,000 requests per second while each individual IP stays perfectly within the limits.</p><p>I needed a way to rate-limit the <em>entire</em> Meta block as a single entity.</p><p>The first step was figuring out exactly which IP ranges belonged to Meta. I took one of the offending IPs from the logs (e.g., <code>57.141.20.57</code>) and ran it through a WHOIS lookup. It pointed me to the RIPE <code>FB-BLOCK</code> range, which covers a few large CIDR blocks: <code>57.141.0.0/16</code>, <code>57.142.0.0/15</code>, <code>57.144.0.0/14</code>, and <code>57.148.0.0/15</code>.</p><p>To implement this in Nginx, I used the <code>geo</code> module to tag these IPs and then the <code>map</code> module to create a shared key for them.</p><div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-nginx data-lang=nginx><span style=display:flex><span><span style=color:#66d9ef>geo</span> $git_is_meta {
</span></span><span style=display:flex><span>    <span style=color:#f92672>default</span> <span style=color:#ae81ff>0</span>;
</span></span><span style=display:flex><span>    <span style=color:#f92672>57.141.0.0/16</span> <span style=color:#ae81ff>1</span>;
</span></span><span style=display:flex><span>    <span style=color:#f92672>57.142.0.0/15</span> <span style=color:#ae81ff>1</span>;
</span></span><span style=display:flex><span>    <span style=color:#f92672>57.144.0.0/14</span> <span style=color:#ae81ff>1</span>;
</span></span><span style=display:flex><span>    <span style=color:#f92672>57.148.0.0/15</span> <span style=color:#ae81ff>1</span>;
</span></span><span style=display:flex><span>}
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span><span style=color:#75715e># If the client is Meta, they all share this single string as their key
</span></span></span><span style=display:flex><span><span style=color:#75715e></span><span style=color:#66d9ef>map</span> $git_is_meta $git_meta_key {
</span></span><span style=display:flex><span>    <span style=color:#f92672>1</span> <span style=color:#e6db74>&#34;meta-crawler&#34;</span>;
</span></span><span style=display:flex><span>    <span style=color:#f92672>0</span> <span style=color:#e6db74>&#34;&#34;</span>; <span style=color:#75715e># Empty key means the limit doesn&#39;t apply to them
</span></span></span><span style=display:flex><span><span style=color:#75715e></span>}
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span><span style=color:#75715e># For everyone else, use their actual IP address as the key
</span></span></span><span style=display:flex><span><span style=color:#75715e></span><span style=color:#66d9ef>map</span> $git_is_meta $git_ip_key {
</span></span><span style=display:flex><span>    <span style=color:#f92672>0</span> $binary_remote_addr;
</span></span><span style=display:flex><span>    <span style=color:#f92672>1</span> <span style=color:#e6db74>&#34;&#34;</span>; <span style=color:#75715e># Empty key means the limit doesn&#39;t apply to them
</span></span></span><span style=display:flex><span><span style=color:#75715e></span>}
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span><span style=color:#75715e># Define the shared bucket for Meta (1 request per second, with a burst of 30)
</span></span></span><span style=display:flex><span><span style=color:#75715e></span><span style=color:#66d9ef>limit_req_zone</span> $git_meta_key <span style=color:#e6db74>zone=git_meta:10m</span> <span style=color:#e6db74>rate=1r/s</span>;
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span><span style=color:#75715e># Define the standard per-IP bucket for everyone else (10 req/s, burst 20)
</span></span></span><span style=display:flex><span><span style=color:#75715e></span><span style=color:#66d9ef>limit_req_zone</span> $git_ip_key <span style=color:#e6db74>zone=git_ip:10m</span> <span style=color:#e6db74>rate=10r/s</span>;</span></span></code></pre></div><p>Then, in the git.sync.wtf server block, I enabled both limits. Nginx applies multiple <code>limit_req</code> directives, and the most restrictive one wins. For a Meta IP, the <code>git_ip</code> limit is disabled (empty key), so it only hits the <code>git_meta</code> limit. For a regular user, the <code>git_meta</code> limit is disabled, so they only hit the <code>git_ip</code> limit.</p><div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-nginx data-lang=nginx><span style=display:flex><span><span style=color:#66d9ef>location</span> <span style=color:#e6db74>/</span> {
</span></span><span style=display:flex><span>    <span style=color:#f92672>limit_req</span> <span style=color:#e6db74>zone=git_meta</span> <span style=color:#e6db74>burst=30</span> <span style=color:#e6db74>delay=8</span>;
</span></span><span style=display:flex><span>    <span style=color:#f92672>limit_req</span> <span style=color:#e6db74>zone=git_ip</span> <span style=color:#e6db74>burst=20</span> <span style=color:#e6db74>nodelay</span>;
</span></span><span style=display:flex><span>    <span style=color:#f92672>limit_req_status</span> <span style=color:#ae81ff>429</span>;
</span></span><span style=display:flex><span>}</span></span></code></pre></div><p>The results were immediate. Before the change, we were seeing around 350,000 requests per day. Since implementing the shared bucket, that has dropped to roughly 86,000 requests per day—a reduction of about 75%—all while keeping the crawler&rsquo;s access to the site functional (it just gets throttled).</p><p>So. No need to give up and/or set extremely short rate limits that also punish regular usage (like cloning a repo) when you&rsquo;re dealing with distributed botnets or crawlers. Don&rsquo;t just look at the individual IPs - look at the whole range, find the owner, and group them into a single bucket. Come to think of it, I&rsquo;m sure there&rsquo;s some list I can just download and automate a lot of this &mldr;</p><hr><p>Why I&rsquo;m not blocking those IP ranges outright? Hey - I <em>want</em> our excellent code to influence all the AI bots.</p>]]></description>
                    

                    
                        <guid isPermaLink="false">tag:blog.troed.se,2026-08-11:/posts/nginx-meta-rate-limiting/</guid>
                    

                    
                        <link>https://blog.troed.se/posts/nginx-meta-rate-limiting/</link>
                    

                    
                        

                        

                        <atom:link href="https://blog.troed.se/posts/nginx-meta-rate-limiting/" hreflang="en-sv" rel="alternate" type="text/html"/>
                    

                    

                    
                        <pubDate>Tue, 11 Aug 2026 12:00:00 CEST</pubDate>
                    

                    
                        <title>Nginx Rate Limiting: Taming the Meta-Crawler</title>
                    
                </item>
            
        
    </channel>
</rss>
