« Back to Web

Rate limit HTTP clients with nginx

Nginx has many options, with one of them the option to rate limit requests. This is a very helpful option ensure HTTP clients behave themselves a little bit. If they don’t, they can be quickly discovered and actions taken. In this article we have a look at how to set this up.

Setting up a rate limited zone

The first step it so set up a zone where rate limiting is applied. With nginx we need to do this in a few steps.

  1. Match IP address to determine if a client is restricted or unrestricted
  2. Map the IP address to leverage its binary notation of the IP address
  3. Define the rate limited zone
  4. Optionally define the error code to use
  5. Apply zone in virtual host

Steps 1-4 can be defined within the http context, so that we can use the rate limiting functionality in multiple virtual hosts. This is normally done in your main nginx configuration file (nginx.conf).

The first steps are split to give a little bit more insights, with a full configuration piece at the end of the section.

Define geo

First we define if a client needs to be rate limited or not. We use the variable $unrestricted_ip for this. If it contains a zero (0), then it is a normal client that should be rate limited. If we define the value 1 to it, then no rate limiting is applied.

http {
           # ... other configuration options ...

           # Define if a client IP is unrestricted (by default it is not)
	   geo $unrestricted_ip {
		default                   0;  # Default set to 0, normal clients are definitely not unrestricted
		proxy 1.2.3.4/24;             # Use this if traffic comes in via a proxy, otherwise remove
		proxy abc:abc:abcd::/48;      # Same, but for IPv6

                ::1                       1;  # localhost
		1.2.3.4/24                1;  # Our proxies should never be limited
                66.249.64.0/19            1;  # We allow Google bot to go nuts
                127.0.0.1                 1;  # localhost
            }
}

In this example we defined our default (0), we added proxies (optional), and defined some hosts and network ranges that should not be rate limited.

Mapping restricted clients for accounting purposes

Next step is to store the IP addresses of normal client in a binary format, so that nginx can process them more efficiently. This is done by using the value from $binary_remote_addr, which is a built-in variable. We tell nginx that if a client is restricted, that the binary representation of the client IP address should be used for accounting purposes. If the client is unrestricted, then empty the value. The result will be stored in variable $restricted_ip_key.

http {
            geo $unrestricted_ip { ...; }

            # Next step: map client IP to the binary notation of the IP address, otherwise empty it
	    map $unrestricted_ip $restricted_ip_key {
                0 $binary_remote_addr; # key set, so rate-limiting applies
                1 '';
            }
}

Create a rate limited zone

Now it is time to create the zone itself. You can give it any name that you want, for this example we call it ratelimitedzone.

For this zone we reserve 10 megabytes of memory, enough to hold a lot of IP addresses. If you have a very busy server, tune this to your needs.

The rate limit is defined with 60 requests per minute, which translated to 1 every second. That is not much, especially considering that HTTP clients typically make multiple requests shortly to pull in the HTML, CSS, JavaScript, fonts, etc. So this needs some fine-tuning when we apply the zone, by allowing a ‘burst’ of requests. If your average web page has around 15 requests, this rate limit of 60 would mean the visitor could browse to 4 pages within 1 minute. Typically a visitor takes the time to read the page and won’t switch that quickly between multiple pages.

The last line defines what error code we want to return. Unfortunately, nginx returns by default a HTTP 503 error, indicating it is a server issue. There is a better response code and that is HTTP 429 or Too Many Requests, exactly what happens when a client performs too many requests in some amount of time. So we use that status code when rate limiting is active.

http {
            geo $unrestricted_ip { ...; }
	    map $unrestricted_ip $restricted_ip_key { ...; }

            # Define our zone with the binary notation of the IP address
            limit_req_zone  $restricted_ip_key  zone=ratelimitedzone:10m   rate=60r/m;
            limit_req_status 429;             # Return HTTP/429 = Too Many Requests (instead of default 503)
}

Full example of the rate limited zone

So if we combine these parts, you get something like this:

http {
           # ... other configuration options ...

           # Define if a client IP is unrestricted (by default it is not)
	   geo $unrestricted_ip {
		default                   0;  # Default set to 0, normal clients are definitely not unrestricted
		proxy 1.2.3.4/24;             # Use this if traffic comes in via a proxy, otherwise remove
		proxy abc:abc:abcd::/48;      # Same, but for IPv6

                ::1                       1;  # Do not restrict localhost
		1.2.3.4/24                1;  # Our proxies should never be limited
                66.249.64.0/19            1;  # We allow Google bot to go nuts
                127.0.0.1                 1;  # Do not restrict localhost
            }

            # Next step: map client IP to the binary notation of the IP address, otherwise empty it
	    map $unrestricted_ip $restricted_ip_key {
                0 $binary_remote_addr; # key set, so rate-limiting applies
                1 '';
            }
            # Define our zone with the binary notation of the IP address
            limit_req_zone  $restricted_ip_key  zone=ratelimitedzone:10m   rate=60r/m;
            limit_req_status 429;             # Return HTTP/429 = Too Many Requests (instead of default 503)
}

This configuration could be simplified by just using the last two lines (with $binary_remote_addr). The downside of that is that all HTTP clients will be rate limited. That may get you into troubles if you have a fairly strict rate defined and “good” clients get rate limited as well. For example, the Google bot typically behaves well, but now and then it wants to do a quick update of many pages and may hit the rate limits. Also you might have services like a link checker that you want to grant full speed. For this reason, it is useful to allow exceptions for those systems.

Configuring the virtual host

With this initial piece of configuration, the next step is applying this configuration within a virtual host. This way the zone will actually be used.


server {
            listen ...;
            server ...;

            # Apply our rate limited zone which is defined within the HTTP context (in /etc/nginx/nginx.conf)
            limit_req zone=ratelimitedzone burst=90 nodelay;
}

Within our virtual host configuration we have now defined that we want to apply rate limiting. We refer to our zone (ratelimitedzone) and can give it some additional options. In this case we allow a burst, meaning that clients can temporarily go beyond the related of 60 requests per minute (= 1 per second). This is useful to allow legitimate clients to pull in all required files for the first time (e.g. CSS, JavaScript) to serve a page. In this example we will not delay any requests (nodelay). See the related moduleExternal link for details about fine-tuning this to your needs.

Not sure if your chosen rate limit is correct to properly catch bad-behaving clients while still allowing legitimate clients? Use the limit_req_dry_run option to do accounting and logging, but not enforcing the limits.

limit_req_dry_run on;

Restart and testing

After implementing the changes, confirm that your nginx configuration looks fine with the nginx command and the -t option:

nginx -t

All good? Restart the server, for Linux that is usually done with systemctl.

systemctl restart nginx.service

Next step is testing the rate limit. You could do this by setting the rate limit values very low and send some requests with curl. Another option is to fire up the ab tool (Apache Bench) and do a short stress test.

Blocking repeating offenders

If you have repeating offenders, then they will be showing up with a 429 status code in the log file. If a client receives this message and back offs, then it actually understood that it was going too quick. But some clients simply ignore it and will continue to perform their requests. This is especially the case for crawlers that want to index as much pages as quickly as possible.

One option is to consider to block the IP addresses that keep hitting the rate limit values. One option is to count the number of 429s per IP address and it goes over a threshold, to block them.

Need more ideas how to do this? Let it know!

References

Relevant commands in this article

Like to learn more about the commands that were used in this article? Have a look, for some there is also a cheat sheet available.

See the full list of Linux commands for additional system administration tools.

Feedback

Small picture of Michael Boelen

This article has been written by our Linux security expert Michael Boelen. With focus on creating high-quality articles and relevant examples, he wants to improve the field of Linux security. No more web full of copy-pasted blog posts.

Discovered outdated information or have a question? Share your thoughts. Thanks for your contribution!

Mastodon icon

Related articles

Like to learn more? Here is a list of articles within the same category or having similar tags.