Site icon GetPageSpeed

NGINX Delay Module: Slow Down Attackers Easily

NGINX Delay Module: Slow Down Attackers Without Blocking Your Server

The NGINX delay module is a lightweight security tool that introduces artificial pauses into NGINX request processing. It slows down brute force attacks, deters vulnerability scanners, and creates tarpits for malicious bots — all without blocking worker processes or degrading performance for legitimate users.

Every server on the public internet faces a constant barrage of automated attacks. Bots probe for exposed .env files, hammer login pages with credential stuffing, and scan for known vulnerabilities — all at machine speed. Traditional defenses like rate limiting and IP blocking either allow a request or reject it. There is no middle ground.

The delay module fills that gap. Instead of outright blocking suspicious requests, it introduces a configurable pause before processing them. A two-second delay on your login page is imperceptible to legitimate users but devastates brute force attacks. A thirty-second tarpit on .env requests ties up scanner resources. And because the module uses NGINX’s asynchronous event loop, these delays cost your server virtually nothing.

How the NGINX Delay Module Works

The delay module operates in NGINX’s preaccess phase — after URL rewriting but before access control checks. When a request hits a location with a configured delay, the module:

  1. Registers a timer in NGINX’s event loop
  2. Suspends request processing (without blocking the worker)
  3. Resumes the request after the specified duration

This is fundamentally different from a sleep() call. NGINX workers continue serving other requests during the delay period. In testing, five concurrent requests to a two-second delay endpoint all completed in approximately two seconds total — not ten. The module is fully non-blocking.

Preaccess Phase and Request Flow

Understanding where the delay module sits in NGINX’s request processing pipeline is important for correct configuration:

Request → Rewrite phase → Preaccess phase (delay here) → Access phase → Content phase → Response

Because the delay runs in the preaccess phase, it works with content handlers like try_files and proxy_pass. However, the return directive executes during the rewrite phase — before the delay — and therefore bypasses the delay entirely. This is an important caveat covered in the troubleshooting section below.

Installation

RHEL, CentOS, AlmaLinux, Rocky Linux

Install the NGINX delay module from the GetPageSpeed RPM repository:

sudo dnf install https://extras.getpagespeed.com/release-latest.rpm
sudo dnf install nginx-module-delay

Then load the module by adding this line at the top of your nginx.conf, before any http {} block:

load_module modules/ngx_http_delay_module.so;

Debian and Ubuntu

First, set up the GetPageSpeed APT repository, then install:

sudo apt-get update
sudo apt-get install nginx-module-delay

On Debian/Ubuntu, the package handles module loading automatically. No load_module directive is needed.

Verify the installation by testing your NGINX configuration:

sudo nginx -t

Configuration Reference

The NGINX delay module provides a single directive:

delay

Property Value
Syntax delay time;
Default — (disabled)
Context http, server, location

Sets the delay duration before a request is processed. Accepts standard NGINX time values: 500ms, 1s, 2s, 30s, etc.

When no delay directive is present, the module is inactive and adds zero overhead.

Context inheritance: A delay directive in the server block applies to all locations within that server unless a location provides its own override. For example:

server {
    delay 1s;  # Default: 1-second delay for all locations

    location /admin/ {
        delay 3s;  # Override: 3-second delay for admin
        proxy_pass http://backend;
    }

    location /api/ {
        # Inherits 1s delay from server
        proxy_pass http://backend;
    }
}

Security Hardening Use Cases

The NGINX delay module shines in security scenarios where you want to penalize suspicious traffic without completely blocking it. Here are the most effective configurations, each verified on a production-equivalent NGINX server.

Brute Force Mitigation on Login Pages

Login pages are the most common target for credential stuffing attacks. A two-second delay per request limits attackers to thirty attempts per minute — down from thousands:

location /wp-login.php {
    delay 2s;
    try_files $uri =404;
    include fastcgi_params;
    fastcgi_pass php-fpm;
}

For a legitimate user, two seconds is a minor inconvenience at most. For an attacker running an automated tool, it makes the attack impractical. This approach works for any login endpoint: /wp-login.php, /admin/login, /user/signin, or similar.

Tarpit for Vulnerability Scanners

Automated scanners probe for exposed configuration files, database dumps, and version control directories. Instead of returning a quick 404, trap these requests in a tarpit. This wastes the scanner’s time and ties up its connection:

location ~ \.(env|git|bak|sql|log)$ {
    delay 30s;
    try_files /dev/null =404;
}

This configuration holds the scanner’s connection open for thirty seconds before returning a 404. During this time, the scanner’s thread or connection slot is occupied. This significantly slows its overall scan rate across your server.

Protecting XML-RPC Endpoints

WordPress’s xmlrpc.php is frequently abused for brute force amplification attacks where a single request can test hundreds of passwords. A secure NGINX configuration for WordPress typically blocks or restricts this endpoint. Adding a delay makes this vector far less attractive:

location = /xmlrpc.php {
    delay 5s;
    try_files $uri =404;
    include fastcgi_params;
    fastcgi_pass php-fpm;
}

Layered Defense with limit_req

The delay module complements NGINX’s built-in limit_req directive. Together, they create a layered defense. The limit_req directive enforces a hard rate ceiling. Meanwhile, the delay module adds latency that degrades attacker performance even within the allowed rate.

limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;

server {
    location /wp-login.php {
        limit_req zone=login burst=3 nodelay;
        delay 2s;
        try_files $uri =404;
        include fastcgi_params;
        fastcgi_pass php-fpm;
    }
}

Here, limit_req permits a maximum of five requests per minute per IP with a burst allowance of three. The delay directive adds a two-second pause to every request, including those that limit_req will ultimately reject.

This creates a security advantage: attackers cannot quickly determine whether they are being rate-limited. Every response takes at least two seconds regardless of the outcome.

Slowing Down Admin Area Access

For administrative interfaces that legitimate users access infrequently, a modest delay adds a security layer with negligible user impact:

location /admin/ {
    delay 1s;
    proxy_pass http://backend;
}

location /wp-admin/ {
    delay 1s;
    try_files $uri =404;
    include fastcgi_params;
    fastcgi_pass php-fpm;
}

How the Delay Module Differs from limit_req

NGINX’s built-in limit_req module and the NGINX delay module serve complementary purposes. Understanding the difference is key to using them effectively:

Feature limit_req NGINX delay module
What it does Limits request rate per key (IP, URI, etc.) Adds fixed latency to every matching request
Excess requests Rejected with 503 (or custom status) All requests processed, just delayed
Attacker detection Immediate 503 reveals rate limiting Consistent delay masks defense
Configuration Zone-based with burst and nodelay options Single time value per location
Overhead Memory for zone tracking Negligible (timer per connection)

Use limit_req when you need to cap throughput. Use the NGINX delay module when you need to degrade attacker speed without revealing your defense. Use both together for maximum protection.

Performance Considerations

The NGINX delay module is designed for production use. Here is what you need to know about its performance characteristics.

Worker Processes Are Not Blocked

The module uses NGINX’s built-in timer mechanism (a binary heap data structure). Delayed requests sit in the event loop — they do not occupy a worker thread or block other connections. In load testing, five hundred concurrent delayed requests had no measurable impact on response times for non-delayed endpoints.

Memory Usage

Each delayed request consumes one timer entry in NGINX’s event loop. This uses a negligible amount of memory. Even thousands of concurrent delayed requests will not materially affect memory usage.

Connection Capacity Planning

While the delay module is lightweight, extremely long delays combined with high traffic volumes will increase the number of open connections. Monitor your worker_connections setting:

events {
    worker_connections 4096;  # Increase if using long delays with high traffic
}

Each delayed request holds one connection open for the duration of the delay. If your typical traffic is 100 requests per second to a delayed endpoint with a 30-second delay, you need capacity for approximately 3,000 concurrent connections.

Troubleshooting

The return Directive Bypasses Delay

The most common misconfiguration is combining delay with return:

# ❌ WRONG — delay has no effect here
location /slow {
    delay 5s;
    return 200 "This response is NOT delayed";
}

The return directive processes in the rewrite phase, which executes before the preaccess phase where the delay module operates. The response is sent immediately.

Fix: Use try_files or proxy_pass instead:

# ✅ CORRECT — delay works with try_files
location /slow {
    delay 5s;
    try_files $uri =404;
}

Verifying the Module Is Loaded

If NGINX reports “unknown directive ‘delay'”, the module is not loaded. Check that you have the load_module line in your nginx.conf:

load_module modules/ngx_http_delay_module.so;

Verify the module file exists:

ls /usr/lib64/nginx/modules/ngx_http_delay_module.so

Then test the configuration:

sudo nginx -t

Measuring the Actual Delay

Use curl with timing output to verify delays are working:

curl -o /dev/null -s -w "TTFB: %{time_starttransfer}s\nTotal: %{time_total}s\n" https://example.com/login

The TTFB (Time To First Byte) should approximately match your configured delay value.

Double Delays on Internal Redirects

The module includes a guard against applying the delay twice when NGINX performs an internal redirect. For example, this guard activates when try_files falls through to index.html via a directory index. However, if you observe unexpectedly long delays, check whether your configuration triggers internal redirects that route through multiple delayed locations.

Real-World Configuration Example

Here is a complete NGINX configuration that demonstrates a security-hardened setup using the NGINX delay module alongside other protection measures:

load_module modules/ngx_http_delay_module.so;

limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
limit_req_zone $binary_remote_addr zone=xmlrpc:10m rate=1r/m;

server {
    listen 443 ssl;
    server_name example.com;
    root /var/www/html;

    # Normal content — no delay
    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    # Login protection: rate limit + delay
    location = /wp-login.php {
        limit_req zone=login burst=3 nodelay;
        delay 2s;
        include fastcgi_params;
        fastcgi_pass unix:/run/php-fpm/www.sock;
    }

    # XML-RPC: aggressive rate limit + longer delay
    location = /xmlrpc.php {
        limit_req zone=xmlrpc burst=1 nodelay;
        delay 5s;
        include fastcgi_params;
        fastcgi_pass unix:/run/php-fpm/www.sock;
    }

    # Tarpit for scanners
    location ~ \.(env|git|bak|sql|log)$ {
        delay 30s;
        try_files /dev/null =404;
    }

    # PHP handling
    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_pass unix:/run/php-fpm/www.sock;
    }
}

Conclusion

The NGINX delay module is a lightweight, non-blocking tool that adds a time-based dimension to your server’s security posture. It excels in scenarios where outright blocking is too aggressive and rate limiting alone is insufficient. By slowing down malicious traffic, you force attackers to spend more time and resources on each attempt. This makes your server a less attractive target overall.

The module is available as a package for RHEL-based distributions and Debian/Ubuntu. The source code is on GitHub.

For additional NGINX security modules, see ModSecurity for NGINX and the nginx-mod enhanced build with extended rate limiting capabilities.

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

Exit mobile version