Nginx Rate Limit

Introduction

Nginx is a powerful web server that can be used to serve static content, load balance, and act as a reverse proxy. It is also capable of rate limiting requests to prevent abuse and protect your server from being overwhelmed.

I seen various guides on how to set up rate limiting in Nginx, but I wanted to write my own since I had a specific use case in mind and I couldn’t find an example anywhere.

I wanted to limit the number of requests to all my subdomains, but I also wanted to allow a single subdomain to bypass the rate limit.

This is useful when you host certain services that may require more requests than others, such as a web applications that hosts static files.

Basic Rate Limiting Example

To set up rate limiting in Nginx, you will need to add a new limit_req_zone directive to your http block. This directive will define a new rate limiting zone that will be used to track the number of requests to your server.

Personally I created a new file in /etc/nginx/conf.d/ called base-rate-limit.conf and added the following:

limit_req_zone $binary_remote_addr zone=rate_limit:10m rate=10r/s;

This will create a new rate limiting zone called rate_limit that will track the number of requests to your server. The 10m parameter specifies the size of the zone, and the 10r/s parameter specifies the rate at which requests will be limited.

Next, you will need to add a new limit_req directive to under the original limit_req_zone block. This directive will use the zone we defined earlier to limit the number of requests to your server.

limit_req_zone $binary_remote_addr zone=rate_limit:10m rate=10r/s;
limit_req zone=rate_limit burst=20 nodelay;

This will limit the number of requests to your server to 10 requests per second, with a burst of 20 requests. The nodelay parameter will prevent requests from being delayed if the rate limit is exceeded.

Changing the default status code

If you are like me and the default status code of 503 is not what you want, you can change it by adding the limit_req_status under the limit_req directive.

limit_req_zone $binary_remote_addr zone=rate_limit:10m rate=10r/s;
limit_req zone=rate_limit burst=20 nodelay;
limit_req_status 429;

HTTP 429 is the status code for “Too Many Requests” and is more appropriate for rate limiting.

Bypass Rate Limiting for a Subdomain

After evaluating my subdomains, I discovered that one was subjected to rate limiting, which was not my intention.

Reading through the Nginx documentation, I found that if you pass an empty string to limit_req_zone it will not create a new zone. However, I still want to track requests for some domains? How can I do this?

The easiest solution I found was creating 2 map functions the first would return integer value based on the $server_name variable and the second would use the integer value to return either an empty string or $binary_remote_addr.

map $server_name $limit {
    default 1;
    subdomain1  0;
}
map $limit $limit_key {
    0 "";
    1 $binary_remote_addr;
}
limit_req_zone $limit_key zone=rate_limit:10m rate=10r/s;
limit_req zone=rate_limit burst=20 nodelay;
limit_req_status 429;

But how do I know what the value of $server_name is? When you define a subdomain in your Nginx configuration, you can use the server_name directive to specify the domain that you want to serve.

Bypass Rate Limiting for a Domain

If you want to bypass rate limiting for a whole domain, you can still use the map functions we created earlier. However, you can provide a modifier to the map function to allow you to match a domain and all its subdomains.

map $server_name $limit {
    hostnames;
    default 1;
    subdomain1  0;
    *.example.com  0;
}
map $limit $limit_key {
    0 "";
    1 $binary_remote_addr;
}
limit_req_zone $limit_key zone=rate_limit:10m rate=10r/s;
limit_req zone=rate_limit burst=20 nodelay;
limit_req_status 429;

As you can see above we use the hostnames; modifier to allow the strings used within the map function to be treated as hostnames. This allows us to use wildcards to match a domain and all its subdomains.

However, the value returned from the map function are based on this heirarchy from the Nginx documentation:

1. string value without a mask
2. longest string value with a prefix mask, e.g. “*.example.com”
3. longest string value with a suffix mask, e.g. “mail.*”
4. first matching regular expression (in order of appearance in a configuration file)
5. default value

Conclusion

Rate limiting is a powerful tool that can be used to protect your server from being overwhelmed by abusive requests. By using the limit_req_zone and limit_req directives, you can limit the number of requests to your server and prevent abuse.

Having the ability to bypass rate limiting for specific subdomains or domains is also useful, as it allows you to host services that may require more requests than others.

I hope this guide has been helpful, and I encourage you to experiment with rate limiting in Nginx to see how it can be used to protect your server.