Skip to main content

NGINX / Security

NGINX max_headers: Prevent Header-Flooding DoS

by , , revisited on


We have by far the largest RPM repository with NGINX module packages and VMODs for Varnish. If you want to install NGINX, Varnish, and lots of useful performance/security software with smooth yum upgrades for production use, this is the repository for you.
Active subscription is required.

Every HTTP request your NGINX server processes carries headers — Host, User-Agent, Accept, cookies, and more. A typical browser sends 5–15 headers per request. However, nothing in standard NGINX prevents a malicious client from sending hundreds or even thousands of small headers in a single request. This is a header-flooding denial-of-service (DoS) attack, and until now, NGINX had no straightforward way to stop it. The NGINX max_headers directive, available in nginx-mod, solves this problem.

The max_headers directive sets a hard limit on the number of HTTP request headers NGINX will accept. Requests that exceed the limit are rejected immediately with a 400 Bad Request response, protecting your server from resource exhaustion.

Why Standard NGINX Is Vulnerable to Header Flooding

Standard NGINX limits request headers only by total size, using the large_client_header_buffers directive. The default setting allows 4 buffers of 8 KB each — a total of 32 KB for all request headers combined.

The problem is that each HTTP header can be very small. A header like X-A: 1 occupies just 6 bytes. An attacker can pack thousands of such tiny headers into 32 KB, forcing NGINX to parse, allocate, and process each one individually. This creates significant CPU and memory overhead without triggering the buffer size limit.

Consider this: 32 KB of buffer space can hold approximately 5,000 minimal headers. Processing that many headers per request is wasteful at best and a denial-of-service vector at worst.

The Gap in NGINX’s Defenses

NGINX provides several mechanisms to limit request processing:

Directive What It Limits Default
client_header_buffer_size Initial header buffer size 1 KB
large_client_header_buffers Max header buffer count and size 4 x 8 KB
client_header_timeout Time to receive the complete header 60s
client_max_body_size Request body size 1 MB
max_headers Number of request header lines 100

Notice that only max_headers addresses the count of headers. The other directives control size and timing but not the number of individual header lines. This is the gap that NGINX max_headers fills.

How the Directive Works

The max_headers directive is a patch applied to the NGINX core in nginx-mod. It adds a simple counter to the request header parsing logic.

As NGINX parses each incoming request header, it increments a counter. If that counter exceeds the configured max_headers value, NGINX immediately:

  1. Logs the message: client sent too many header lines
  2. Closes the connection with lingering close (for HTTP/1.x)
  3. Returns HTTP status 400 Bad Request

This enforcement works across all HTTP protocol versions:

  • HTTP/1.1 — checked during ngx_http_process_request_headers()
  • HTTP/2 — checked during HPACK header decompression
  • HTTP/3 — checked during QPACK header decompression

The default limit of 100 headers is deliberately generous. Normal browsers send far fewer headers, even with numerous cookies. A limit of 100 provides a large safety margin for legitimate traffic while blocking clearly malicious header flooding.

Installation

The max_headers directive is available in nginx-mod 1.28.2-24 and later. Install it on RHEL-based systems (Rocky Linux, AlmaLinux, CentOS, RHEL, Fedora) from the GetPageSpeed repository:

sudo dnf install https://extras.getpagespeed.com/release-latest.rpm
sudo dnf config-manager --enable getpagespeed-extras-nginx-mod
sudo dnf install nginx-mod

Verify the installation:

nginx -v

You should see:

nginx version: nginx-mod by GetPageSpeed.com/1.28.2

No load_module statement is needed. The directive is built directly into the nginx-mod binary. It is not a dynamic module — it is a core patch.

Configuration

Syntax

max_headers number;
  • Context: http, server
  • Default: 100

The directive accepts a single numeric argument — the maximum number of HTTP request header lines allowed per request.

Basic Usage

In most cases, the default value of 100 provides adequate protection. You do not need to add any configuration to benefit from max_headers — it is active by default in nginx-mod.

However, if you want to set a stricter limit, add the directive to your server or http block:

http {
    # Apply a global limit of 50 headers to all virtual hosts
    max_headers 50;

    server {
        listen 80;
        server_name example.com;
        # Inherits max_headers 50 from the http block
    }

    server {
        listen 80;
        server_name api.example.com;
        # Override with a higher limit for API server
        max_headers 80;
    }
}

Choosing the Right Value

To determine an appropriate value for your server, review your application requirements:

Application Type Typical Header Count Recommended max_headers
Static file server 5–10 30
Standard web application 10–20 50
API with authentication tokens 15–30 60
Application with many cookies 20–40 80
Default (general purpose) Varies 100

Important: Not Allowed in location Context

Unlike many NGINX directives, max_headers is only valid in the http and server contexts. Placing it inside a location block will cause a configuration error:

nginx: [emerg] "max_headers" directive is not allowed here

This is by design. Header counting occurs during the request header parsing phase, which happens before NGINX determines which location block will handle the request.

Testing the Directive

After configuring max_headers, verify it works correctly. First, set a low limit for testing purposes:

server {
    listen 80;
    server_name test.example.com;
    max_headers 5;

    location / {
        return 200 "OK\n";
    }
}

Validate and reload:

sudo nginx -t && sudo systemctl reload nginx

Now send a request within the limit. Curl sends three default headers (Host, User-Agent, Accept), so two additional custom headers should still be within the limit:

curl -i -H "X-Test-1: a" -H "X-Test-2: b" http://test.example.com/

Expected response: HTTP/1.1 200 OK

Then send a request that exceeds the limit:

curl -i \
  -H "X-H1: a" -H "X-H2: b" -H "X-H3: c" \
  -H "X-H4: d" -H "X-H5: e" -H "X-H6: f" \
  http://test.example.com/

Expected response: HTTP/1.1 400 Bad Request with the body “Request Header Or Cookie Too Large”.

Check the error log (at info level) for the rejection message:

sudo grep "too many header lines" /var/log/nginx/error.log

You will see a log entry like:

2026/02/09 03:10:11 [info] 2601#2601: *5 client sent too many header lines
while reading client request headers, client: 127.0.0.1, server: test.example.com,
request: "GET / HTTP/1.1", host: "test.example.com"

Note that the log level is info. If your error_log directive uses the default level of error, these rejections will not appear in the log. To capture them, set a lower log level:

error_log /var/log/nginx/error.log info;

Alternatively, monitor rejections via the access log by watching for 400 responses.

Remember to restore your production value after testing. The default of 100 is recommended for most deployments.

Header Flooding: Understanding the Attack

A header-flooding DoS attack exploits NGINX’s willingness to parse an unlimited number of request headers. Here is how the attack works:

  1. The attacker opens a TCP connection to the NGINX server
  2. Sends a valid HTTP request line: GET / HTTP/1.1\r\n
  3. Follows with thousands of small headers: X-A: 1\r\nX-B: 2\r\n...
  4. Each header is parsed, allocated in memory, and added to a linked list
  5. The server wastes CPU cycles processing headers that serve no legitimate purpose

This attack is particularly effective because:

  • It bypasses rate limiting — each connection sends just one request with many headers, so limit_req does not trigger
  • It stays within buffer limits — thousands of tiny headers fit within the default 32 KB header buffer
  • It is hard to detect — the request looks syntactically valid, just with an unusual number of headers
  • It wastes memory — each header requires a ngx_table_elt_t structure allocation (approximately 64 bytes per header on 64-bit systems)

The max_headers directive stops this attack at the source by counting headers during parsing and rejecting the request before wasting additional resources.

How max_headers Compares to Other Defenses

Defense Layer Mechanism Protects Against
max_headers Limits header count per request Header flooding
large_client_header_buffers Limits total header memory Oversized headers
client_header_timeout Limits header receive time Slowloris attacks
Rate limiting Limits requests per time window Request flooding
ModSecurity WAF Inspects request content Application-layer attacks

These defenses are complementary. For a comprehensive security posture, combine max_headers with rate limiting and a web application firewall.

Performance Considerations

The max_headers directive has negligible performance impact on legitimate traffic. The header counter is a simple integer increment — a single CPU instruction — checked during header parsing that already occurs for every request.

For malicious traffic, the performance impact is positive: requests with excessive headers are rejected early, before NGINX processes them further. This saves CPU, memory, and connection resources that would otherwise be wasted.

There is no measurable latency increase for normal requests. The counter check adds less overhead than a single hash table lookup.

Security Best Practices

Follow these recommendations when deploying NGINX max_headers in production:

1. Keep the default value of 100 unless you have a specific reason to change it. Most web applications send fewer than 30 headers. The default of 100 provides a generous buffer.

2. Combine max_headers with buffer size limits. The two protections address different attack vectors:

server {
    listen 80;
    server_name example.com;

    max_headers 50;
    large_client_header_buffers 4 8k;

    # ...
}

3. Monitor for rejections. If legitimate users are being blocked, increase the limit. Check your access log for unexpected 400 responses:

grep " 400 " /var/log/nginx/access.log | tail -20

4. Use rate limiting alongside max_headers. While the directive limits per-request overhead, rate limiting restricts how many requests a single client can make:

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

server {
    listen 80;
    server_name example.com;
    max_headers 50;

    location / {
        limit_req zone=general burst=20 nodelay;
        # ...
    }
}

5. Audit your security headers configuration. While max_headers protects against incoming header abuse, make sure your server also sends proper outgoing security headers.

Troubleshooting

“max_headers” directive is not allowed here

This error occurs when you place max_headers inside a location block. Move it to the server or http block instead. Header counting happens during the header parsing phase, before location matching.

Legitimate requests returning 400

If real users receive 400 responses after enabling a custom max_headers value, the limit is too low. Increase it. Common causes of high header counts include:

  • Many cookies — each cookie adds headers. Sites with analytics, A/B testing, and advertising scripts can accumulate 20+ cookie headers.
  • Authentication tokens — OAuth, JWT, and SAML tokens in headers add to the count.
  • CDN or proxy headersX-Forwarded-For, X-Real-IP, CF-* headers from upstream proxies.

No log entries for rejected requests

The rejection is logged at info level. Ensure your error_log directive uses info or lower:

error_log /var/log/nginx/error.log info;

Note that info level logging generates more output. In high-traffic environments, consider using warn level and relying on the access log (400 status codes) instead.

max_headers 0 blocks all requests

Setting max_headers 0; effectively rejects every request, because even the first header (Host) triggers the limit. This is rarely useful. Always set the value to at least 10.

Conclusion

The NGINX max_headers directive in nginx-mod closes an important gap in request validation. By limiting the number of HTTP headers per request, it provides a simple and efficient defense against header-flooding DoS attacks across HTTP/1.1, HTTP/2, and HTTP/3.

For most deployments, the default limit of 100 headers requires no configuration. For security-hardened environments, a lower value like 50 is recommended alongside rate limiting and WAF protection.

Install nginx-mod from the GetPageSpeed repository and benefit from this protection immediately.

D

Danila Vershinin

Founder & Lead Engineer

NGINX configuration and optimizationLinux system administrationWeb performance engineering

10+ years NGINX experience • Maintainer of GetPageSpeed RPM repository • Contributor to open-source NGINX modules

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

This site uses Akismet to reduce spam. Learn how your comment data is processed.