Site icon GetPageSpeed

NGINX Perl Module: Embed Perl Scripting in Your Web Server

NGINX Perl Module: Embed Perl Scripting in Your Web Server

The NGINX Perl module (ngx_http_perl_module) lets you embed Perl code directly inside your NGINX configuration. You can generate dynamic responses, compute variables on the fly, and process POST request bodies. You get access to the entire Perl ecosystem — all without spawning a separate application server or FastCGI process.

If you have ever needed a lightweight dynamic endpoint inside NGINX — perhaps a health check, a request router, or a custom response header computed from request data — the NGINX Perl module is a practical solution.

Why Use the NGINX Perl Module?

System administrators often face situations where NGINX’s built-in directives are not expressive enough. Yet deploying a full application backend feels like overkill. Common scenarios include:

The NGINX Perl module runs inside the NGINX worker process itself. There is no inter-process communication overhead. Perl code executes with direct access to the request object.

How the NGINX Perl Module Works

When NGINX starts, it initializes an embedded Perl interpreter within each worker process. During configuration, NGINX evaluates any Perl code specified — loading external modules, compiling inline handlers, and registering variable handlers.

At request time, NGINX calls the appropriate Perl subroutine for the matched location. The subroutine receives a request object ($r) with methods to read request data, set response headers, and send output. The Perl handler runs synchronously within the NGINX event loop. Each handler should complete quickly to avoid blocking other connections.

For long-running tasks, the module provides an asynchronous $r->sleep() method. This yields control back to NGINX’s event loop and resumes the handler after a specified delay.

Installation

RHEL, CentOS, AlmaLinux, Rocky Linux

Install the GetPageSpeed repository and the NGINX Perl module package:

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

Then load the module by adding this line at the very top of /etc/nginx/nginx.conf, before any other directives:

load_module modules/ngx_http_perl_module.so;

Verify the module loads correctly:

nginx -t

If the test passes, the NGINX Perl module is ready for use.

For more details and available versions, see the NGINX Perl module package page.

Note: The NGINX Perl module is currently available only for RHEL-based distributions through the GetPageSpeed repository.

Configuration Directives

The NGINX Perl module provides four directives. The configuration-level directives (perl_modules, perl_require, perl_set) belong in the http {} block. The perl handler directive belongs in a location {} or limit_except {} block.

perl_modules

Adds a directory to Perl’s module search path (@INC). Use this to tell NGINX where to find your custom Perl modules.

Syntax:

perl_modules path;

Context: http

Example:

http {
    perl_modules /etc/nginx/perl;
    # ...
}

You can specify perl_modules multiple times to add several directories. Each directory is appended to @INC before any perl_require statements run.

perl_require

Loads a Perl module file at startup. This is equivalent to Perl’s require statement and runs during the configuration phase.

Syntax:

perl_require module_file;

Context: http

Example:

http {
    perl_modules /etc/nginx/perl;
    perl_require handlers.pm;
    # ...
}

Use perl_require to load external handler modules that define subroutines referenced by perl. The module file must return a true value (end with 1;) just like any standard Perl module.

perl

Assigns a Perl handler subroutine to a location. When a request matches, NGINX calls this subroutine with the request object.

Syntax:

perl module::handler;
perl 'sub { ... }';

Context: location, limit_except

Examples:

Reference an external subroutine:

location /api {
    perl handlers::json_response;
}

Use an inline handler:

location /hello {
    perl 'sub {
        my $r = shift;
        $r->send_http_header("text/plain");
        $r->print("Hello, World!\n");
        return OK;
    }';
}

The handler receives a single argument — the NGINX request object $r. It must return a status code: OK for success, DECLINED to pass to the next handler, or an HTTP status code like HTTP_NOT_FOUND.

perl_set

Defines an NGINX variable whose value is computed by a Perl subroutine. The subroutine runs each time NGINX evaluates the variable.

Syntax:

perl_set $variable 'sub { ... }';

Context: http

Example:

perl_set $greeting 'sub {
    my $r = shift;
    my $hour = (localtime)[2];
    if ($hour < 12) {
        return "Good morning";
    } elsif ($hour < 18) {
        return "Good afternoon";
    } else {
        return "Good evening";
    }
}';

Variables defined with perl_set work anywhere NGINX variables are accepted — in add_header, return, log_format, if conditions, and more. However, variable handlers cannot modify the response. They must only return a string value.

The Perl Request Object API

Every Perl handler receives a request object $r with methods for reading request data and sending responses. Here is the complete API.

Reading Request Data

Method Returns Description
$r->uri String The request URI path (without query string)
$r->args String The query string
$r->request_method String HTTP method (GET, POST, etc.)
$r->remote_addr String Client IP address
$r->header_in("Name") String Value of a request header (case-insensitive)
$r->header_only 0 or 1 Returns 1 for HEAD requests
$r->request_body String or undef The request body content
$r->request_body_file String or undef Path to temporary file if body was stored on disk
$r->filename String The filesystem path NGINX mapped for this URI

Sending Responses

Method Parameters Description
$r->status(code) HTTP status code Set response status (call before send_http_header)
$r->send_http_header(type) Optional content type Send response headers to client
$r->header_out("Name", "Value") Header name and value Add a response header
$r->print(@args) Strings to output Send response body content
$r->sendfile(path, offset, bytes) File path, optional offset/length Send a file using zero-copy I/O
$r->flush None Flush buffered output
$r->allow_ranges None Enable byte-range request support

Flow Control

Method Parameters Description
$r->internal_redirect(uri) Target URI Redirect internally (can target named locations)
$r->has_request_body(\&handler) Callback subroutine Read the request body, then call handler
$r->discard_request_body None Discard request body (cleanup)
$r->sleep(ms, \&handler) Milliseconds, callback Pause execution and resume after delay

Utility Methods

Method Parameters Description
$r->variable("name") Variable name Get an NGINX variable value
$r->variable("name", "value") Variable name and value Set an NGINX variable value
$r->unescape(text) URI-encoded string Decode URI-encoded text
$r->log_error(errno, message) Error number, message Write to NGINX error log

Practical Use Cases

Dynamic JSON API Endpoint

Create a lightweight API endpoint that returns request information as JSON:

http {
    perl_modules /etc/nginx/perl;
    perl_require handlers.pm;

    server {
        listen 80;

        location /api {
            perl handlers::json_response;
        }
    }
}

Create the handler at /etc/nginx/perl/handlers.pm:

package handlers;

use nginx;

sub json_response {
    my $r = shift;
    $r->header_out("Content-Type", "application/json");
    $r->send_http_header("application/json");

    my $uri = $r->uri;
    my $method = $r->request_method;
    my $args = $r->args || "";

    $r->print("{\"uri\":\"$uri\",\"method\":\"$method\",\"args\":\"$args\"}\n");
    return OK;
}

1;

Test the endpoint:

curl -s "http://localhost/api?user=admin"

Output:

{"uri":"/api","method":"GET","args":"user=admin"}

Processing POST Request Bodies

Handle form submissions or webhook payloads directly in NGINX:

sub post_handler {
    my $r = shift;

    if ($r->request_method eq "POST") {
        $r->has_request_body(\&handle_body);
        return OK;
    }

    $r->send_http_header("text/plain");
    $r->print("Send a POST request to this endpoint.\n");
    return OK;
}

sub handle_body {
    my $r = shift;
    my $body = $r->request_body;

    $r->send_http_header("text/plain");
    $r->print("Received body: $body\n");
    $r->print("Body length: " . length($body) . "\n");
    return OK;
}

The has_request_body method is essential for POST handling. It ensures the body is fully read before your callback executes. Without it, $r->request_body may return undef.

Test it:

curl -s -X POST -d "username=admin&action=login" http://localhost/post

Output:

Received body: username=admin&action=login
Body length: 27

Dynamic Variables with perl_set

Use perl_set to compute NGINX variables that standard directives cannot express. These variables integrate seamlessly with all NGINX directives. For simpler computations, consider the NGINX Let module for arithmetic and hashing:

http {
    perl_set $greeting 'sub {
        my $r = shift;
        my $hour = (localtime)[2];
        if ($hour < 12) {
            return "Good morning";
        } elsif ($hour < 18) {
            return "Good afternoon";
        } else {
            return "Good evening";
        }
    }';

    perl_set $perl_req_id 'sub {
        return sprintf("%08x", int(rand(0xFFFFFFFF)));
    }';

    server {
        listen 80;

        add_header X-Greeting $greeting;
        add_header X-Request-ID $perl_req_id;

        location /greeting {
            return 200 "$greeting, visitor!\n";
        }
    }
}

Test the greeting endpoint:

curl -s http://localhost/greeting

Output (varies by time of day):

Good afternoon, visitor!

The custom headers appear on every response:

curl -sI http://localhost/ | grep "X-"
X-Greeting: Good afternoon
X-Request-ID: 1d90d890

Each request gets a unique request ID. The greeting changes based on server time. Variables from perl_set are evaluated lazily — the subroutine only runs when NGINX needs the value.

Custom Response Headers from Request Data

Add response headers derived from request properties. This is useful for debugging proxy setups or tracking request flow through your NGINX infrastructure:

location /custom-header {
    perl 'sub {
        my $r = shift;
        $r->header_out("X-Powered-By", "NGINX-Perl");
        $r->header_out("X-Request-URI", $r->uri);
        $r->send_http_header("text/plain");
        $r->print("Check response headers!\n");
        return OK;
    }';
}
curl -sI http://localhost/custom-header | grep "X-"
X-Powered-By: NGINX-Perl
X-Request-URI: /custom-header

Serving Files with Zero-Copy I/O

Use $r->sendfile() to serve files efficiently without copying data through Perl:

sub file_server {
    my $r = shift;
    my $filename = $r->filename;

    if (-f $filename) {
        $r->allow_ranges;
        $r->send_http_header;
        $r->sendfile($filename);
        return OK;
    }

    return DECLINED;
}

The sendfile method uses the kernel’s zero-copy mechanism. It is as fast as NGINX’s native static file serving. The allow_ranges call enables byte-range requests for resumable downloads.

Asynchronous Delayed Responses

The $r->sleep() method pauses a handler without blocking the NGINX event loop. Use it for rate limiting, delayed responses, or polling:

sub delayed_response {
    my $r = shift;
    $r->sleep(1000, \&after_sleep);
    return OK;
}

sub after_sleep {
    my $r = shift;
    $r->send_http_header("text/plain");
    $r->print("Response after 1 second delay\n");
    return OK;
}

During the sleep period, the NGINX worker continues processing other requests. The callback fires after the specified number of milliseconds.

Internal Redirects

Route requests to different locations based on Perl logic:

sub conditional_redirect {
    my $r = shift;
    my $target = $r->args;

    if ($target && $target =~ /^redirect=(.+)/) {
        $r->internal_redirect($1);
        return OK;
    }

    $r->send_http_header("text/plain");
    $r->print("No redirect target. Use ?redirect=/path\n");
    return OK;
}

Internal redirects work like NGINX’s rewrite ... last directive. The request is re-processed against the location blocks with the new URI. You can also redirect to named locations (@name).

Writing to the NGINX Error Log

Use $r->log_error() for structured error logging from Perl handlers:

sub error_logger {
    my $r = shift;
    $r->log_error(0, "Perl handler called for URI: " . $r->uri);
    $r->send_http_header("text/plain");
    $r->print("Error logged.\n");
    return OK;
}

The log entry appears in the NGINX error log with full request context:

2026/03/17 08:55:55 [error] 16425#16425: *18 perl: Perl handler called for URI: /log-test, client: 127.0.0.1, server: localhost, request: "GET /log-test HTTP/1.1", host: "localhost"

Available Constants

The NGINX Perl module exports HTTP status constants for return values and $r->status():

Constant Value Constant Value
OK 0 HTTP_BAD_REQUEST 400
DECLINED -5 HTTP_UNAUTHORIZED 401
HTTP_OK 200 HTTP_FORBIDDEN 403
HTTP_CREATED 201 HTTP_NOT_FOUND 404
HTTP_NO_CONTENT 204 HTTP_NOT_ALLOWED 405
HTTP_MOVED_PERMANENTLY 301 HTTP_INTERNAL_SERVER_ERROR 500
HTTP_MOVED_TEMPORARILY 302 HTTP_BAD_GATEWAY 502
HTTP_SEE_OTHER 303 HTTP_SERVICE_UNAVAILABLE 503
HTTP_NOT_MODIFIED 304 HTTP_GATEWAY_TIME_OUT 504

Import these constants by adding use nginx; at the top of your Perl module.

SSI Integration

The NGINX Perl module integrates with Server-Side Includes (SSI). You can call Perl subroutines from SSI-processed HTML:

<!--# perl sub="handlers::timestamp" -->

This calls the handlers::timestamp subroutine and inserts its output into the HTML. SSI integration is useful for injecting dynamic content into otherwise static pages.

Performance: NGINX Perl Module vs njs Benchmarks

A common question is whether the NGINX Perl module is slower than njs. We benchmarked both on Rocky Linux 10 with NGINX 1.28.2, running identical handlers side by side on the same server. Each test used ab with 30,000 requests at concurrency 50, repeated three times and averaged.

Benchmark Setup

Both modules implemented three identical handlers:

  1. Hello World: Return a plain-text "Hello, World!\n" response
  2. JSON response: Build and return a JSON object with URI, method, and remote address
  3. Variable computation: Compute a time-of-day greeting via perl_set / js_set

A static return 200 served as the baseline to isolate scripting overhead.

Results

Handler Static baseline Perl module njs module
Hello World ~38,500 req/s ~40,000 req/s ~40,000 req/s
JSON response ~38,500 req/s ~40,000 req/s ~41,500 req/s
Variable (perl_set / js_set) ~38,500 req/s ~41,100 req/s ~39,100 req/s

Key Takeaways

For simple handlers, Perl and njs perform nearly identically. Both add negligible overhead compared to a static return. All results cluster within 5% of the baseline. There is no meaningful speed difference for typical use cases.

The performance gap widens in complex scenarios:

If your handlers are fast (under 10ms), choose between Perl and njs based on language preference and ecosystem needs — not raw performance.

Comparison with njs and Lua

Here is how the three NGINX scripting options compare beyond raw performance:

Feature Perl Module njs (JavaScript) Lua (OpenResty)
Language Perl 5 JavaScript (ES5.1+) Lua / LuaJIT
Part of NGINX core Yes Separate module Third-party
Async/non-blocking Limited ($r->sleep) Full (Promises, subrequests) Full (coroutines, cosockets)
Ecosystem Full CPAN access Limited built-in modules LuaRocks + OpenResty libs
Simple handler throughput ~40,000 req/s ~40,000 req/s Similar (LuaJIT)
perl_set equivalent perl_set js_set set_by_lua
Handler equivalent perl js_content content_by_lua
Non-blocking HTTP calls No Yes (r.subrequest) Yes (ngx.location.capture)
Best for Sysadmins who know Perl Modern async scripting High-concurrency apps

Choose the NGINX Perl module when you need Perl expertise or CPAN access, or when you want a core NGINX module. Choose njs for non-blocking subrequests or JavaScript. Choose Lua/OpenResty for highest concurrency with cosocket support. For more on the Lua alternative, see our guide on adding Lua scripting to NGINX.

Security Best Practices

When embedding Perl in NGINX, follow these guidelines:

Troubleshooting

“unknown directive perl”

The module is not loaded. Verify that load_module modules/ngx_http_perl_module.so; appears at the top of nginx.conf, before the events block:

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

“Can’t locate handlers.pm in @INC”

Your Perl module file is not in a directory listed by perl_modules. Verify the path:

ls /etc/nginx/perl/handlers.pm

Ensure perl_modules /etc/nginx/perl; appears in your http {} block.

“perl_set: duplicate variable”

You defined a variable that already exists as a built-in NGINX variable. Rename it — for example, use $perl_req_id instead of $request_id.

Handler returns empty response

Call $r->send_http_header() before $r->print(). Without sending headers first, the response body is discarded.

POST body is undef

Use $r->has_request_body(\&callback) to read the body first. Calling $r->request_body directly without this step returns undef.

High memory usage per worker

Each worker runs its own Perl interpreter. Heavy CPAN modules multiply memory by worker count. Profile with:

ps -o pid,rss,command -p $(pgrep -f "nginx: worker")

Reduce worker_processes or simplify your Perl modules to lower memory use.

Conclusion

The NGINX Perl module provides a direct way to embed scripting logic inside your NGINX configuration. For system administrators who need dynamic behavior without a separate backend — computing variables, building API endpoints, or handling POST bodies — it delivers with minimal setup.

Our benchmarks show the NGINX Perl module matches njs performance for typical handlers. Your choice between the two should be driven by language preference and ecosystem needs, not speed.

Every configuration example and benchmark in this article has been verified at runtime with curl and ab on Rocky Linux 10 with NGINX 1.28.2 and Perl 5.40.2.

Install the module from the GetPageSpeed repository and start extending your NGINX configuration with Perl today.

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