Nginx Rate Limiting: Add rate limit for high-frequency 404 and 400 scan requests

A practical guide to Nginx limit_req and limit_conn: how to rate-limit suspicious scan paths, high-frequency 404/400 requests, and per-IP concurrent access, with notes on where limit_req_zone belongs and what the common parameters mean.

If your website logs suddenly show a large number of 404 and 400 responses, the cause is often not normal users clicking broken links. It is usually an automated scanner probing paths such as .env, .git, wp-admin, phpmyadmin, and xmlrpc.php.

These requests create several problems:

  • access log grows quickly
  • error log fills with useless noise
  • static sites or reverse proxy services waste connections on invalid requests
  • real issues get buried under scan noise

Nginx can use limit_req and limit_conn to control this. But first, one point matters: Nginx cannot natively rate-limit directly by “response status code is 404 or 400”, because rate limiting happens before the response is generated.

The practical approach is to rate-limit scan paths, suspicious sources, and high-frequency site-wide requests before they produce 404 / 400 responses.

Basic idea

Use three layers:

  1. Apply gentle site-wide rate limiting to prevent one IP from hammering the site.
  2. Apply strict rate limiting to common scan paths and return 404 directly.
  3. Limit concurrent connections per IP.

A safer rollout order is: first add the scan path rule and access_log off, observe for one day, and only add site-wide limit_req if there are still many random-path 404 requests.

Define rate-limit zones in http first

limit_req_zone and limit_conn_zone must be placed inside http {}. They cannot be placed inside a single site’s server {}.

You can add them directly to the http {} block in /etc/nginx/nginx.conf:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
http {
    # 按客户端 IP 限速,普通页面请求
    limit_req_zone $binary_remote_addr zone=perip_general:20m rate=5r/s;

    # 更严格:疑似扫描路径
    limit_req_zone $binary_remote_addr zone=perip_scan:20m rate=1r/s;

    # 并发连接限制
    limit_conn_zone $binary_remote_addr zone=addr_conn:20m;

    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}

You can also create a new file:

1
sudo nano /etc/nginx/conf.d/limit-zones.conf

Write:

1
2
3
limit_req_zone $binary_remote_addr zone=perip_general:20m rate=5r/s;
limit_req_zone $binary_remote_addr zone=perip_scan:20m rate=1r/s;
limit_conn_zone $binary_remote_addr zone=addr_conn:20m;

This assumes your nginx.conf really includes this inside http {}:

1
include /etc/nginx/conf.d/*.conf;

Then use the zones in server

A site config file is usually something like /etc/nginx/sites-enabled/www.example.com, and it usually contains server {}. Do not write limit_req_zone there. Only use zones already defined earlier.

Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
server {
    root /srv/www/example.com;
    index index.html;
    server_name example.com www.example.com;
    access_log /var/log/nginx/example.com.access.log;
    error_log /var/log/nginx/example.com.error.log;

    # 全站温和限速:允许短时突发,降低误伤正常用户的概率
    limit_req zone=perip_general burst=30 nodelay;
    limit_conn addr_conn 20;

    # 对常见扫描路径严格限速,并关闭访问日志
    location ~* ^/(\.env|\.git|\.svn|wp-|wp/|adminer|phpmyadmin|pma|vendor|backup|config|server-status|cgi-bin|xmlrpc\.php) {
        access_log off;
        limit_req zone=perip_scan burst=5 nodelay;
        return 404;
    }

    # 你原来的 location /、listen ssl 等配置继续放这里
}

If you are worried about hurting normal traffic with site-wide rate limiting, start with only the scan path rule:

1
2
3
4
5
location ~* ^/(\.env|\.git|\.svn|wp-|wp/|adminer|phpmyadmin|pma|vendor|backup|config|server-status|cgi-bin|xmlrpc\.php) {
    access_log off;
    limit_req zone=perip_scan burst=5 nodelay;
    return 404;
}

What these parameters mean

This line:

1
limit_req_zone $binary_remote_addr zone=perip_general:20m rate=5r/s;

means:

  • limit_req_zone: defines the accounting zone for request rate limiting.
  • $binary_remote_addr: uses client IP as the rate-limit key, and uses less memory than $remote_addr.
  • zone=perip_general:20m: creates a shared memory zone named perip_general with size 20m.
  • rate=5r/s: each IP is allowed an average of 5 requests per second.

This line:

1
limit_req_zone $binary_remote_addr zone=perip_scan:20m rate=1r/s;

is similar, but stricter:

  • perip_scan: used specifically for suspicious scan paths.
  • rate=1r/s: each IP is allowed only 1 request per second.

This line:

1
limit_conn_zone $binary_remote_addr zone=addr_conn:20m;

means:

  • limit_conn_zone: defines the accounting zone for concurrent connection limits.
  • $binary_remote_addr: still counts by client IP.
  • zone=addr_conn:20m: creates a connection-counting shared memory zone named addr_conn.

The actual concurrent connection limit is:

1
limit_conn addr_conn 20;

It means each IP can have at most 20 simultaneous connections.

Understanding burst and nodelay

For example:

1
limit_req zone=perip_general burst=30 nodelay;

You can read it like this:

  • rate=5r/s: the long-term average rate is 5 requests per second.
  • burst=30: allow 30 extra requests during a short burst.
  • nodelay: when requests exceed the average rate but are still within burst, process them immediately instead of queueing; reject only after burst is exceeded.

Without nodelay, Nginx tries to delay and queue some requests. For ordinary web pages, nodelay is usually easier to reason about. For APIs or especially sensitive endpoints, adjust according to actual behavior.

Common error: limit_req_zone in the wrong place

If you see this error:

1
2026/04/30 21:33:48 [emerg] 2290771#2290771: "limit_req_zone" directive is not allowed here in /etc/nginx/sites-enabled/example.com:9

It means limit_req_zone was written in a context where it is not allowed.

A common incorrect configuration is placing it inside server {}:

1
2
3
4
5
6
7
8
9
server {
    root /srv/www/example.com;
    index index.html;
    server_name example.com www.example.com;

    limit_req_zone $binary_remote_addr zone=perip_general:20m rate=5r/s;
    limit_req_zone $binary_remote_addr zone=perip_scan:20m rate=1r/s;
    limit_conn_zone $binary_remote_addr zone=addr_conn:20m;
}

This will not work.

One-line memory aid:

  • limit_req_zone defines the pool, so put it in http {}.
  • limit_req uses the pool, so put it in server {} or location {}.
  • limit_conn_zone defines the connection pool, so put it in http {}.
  • limit_conn uses the connection pool, so put it in server {} or location {}.

Temporarily block clearly abnormal IPs

If the logs confirm that several IPs are continuously sending scan requests, you can temporarily block them:

1
2
3
4
5
deny 45.95.42.164;
deny 185.177.72.51;
deny 185.177.72.5;
deny 185.177.72.56;
deny 185.177.72.58;

These deny directives can be placed inside server {} or a specific location {}. Whether to keep them long term depends on false-positive risk and traffic sources.

Check and reload

Check the configuration first:

1
sudo nginx -t

If there is no error, reload:

1
sudo systemctl reload nginx

Do not restart the service directly. reload lets Nginx load the new configuration gracefully, which is safer.

For a personal site or static site, start with:

  • normal pages: rate=5r/s to 10r/s
  • scan paths: rate=1r/s
  • scan path burst=5
  • site-wide burst=30
  • per-IP concurrency: 10 to 20

If normal traffic is very low, the settings can be stricter. If the site has many images, scripts, or API requests, loosen the normal page limit to avoid hurting real users.

The safest approach is a staged rollout:

  1. First apply access_log off + return 404 to scan paths.
  2. Then add strict perip_scan rate limiting.
  3. Observe logs for one day.
  4. If random-path 404s are still heavy, enable gentle site-wide rate limiting.
记录并分享
Built with Hugo
Theme Stack designed by Jimmy