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:
- Dynamic variable computation: Calculate values at request time that NGINX’s
mapdirective cannot express, such as time-based greetings or cryptographic hashes - Lightweight API endpoints: Return JSON or plain-text responses without a separate backend process
- Request inspection and routing: Examine headers, query strings, or POST bodies and make routing decisions in Perl
- Custom logging and error handling: Log to external systems or generate custom error pages with request-specific context
- Integration with Perl libraries: Access any CPAN module for tasks like URL encoding, date formatting, or database lookups
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:
- Hello World: Return a plain-text
"Hello, World!\n"response - JSON response: Build and return a JSON object with URI, method, and remote address
- 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:
- CPU-intensive computation: njs uses a purpose-built JavaScript VM optimized for NGINX. Perl uses a full general-purpose interpreter. For tight loops, njs may have a slight edge.
- Async I/O: njs supports non-blocking subrequests and Promises. Perl’s
$r->sleep()can delay execution, but cannot do non-blocking I/O. - Memory: Perl’s interpreter is larger than njs’s VM. With many workers, Perl uses more resident memory per process.
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:
- Validate all input: Never trust
$r->args,$r->header_in(), or$r->request_bodywithout validation. Sanitize before using in file paths or output. - Avoid
evaland backticks: Do not useeval,system(), or backtick operators in handlers. These introduce code injection risks. - Restrict file access: Validate paths to prevent directory traversal when reading files based on user input.
- Use absolute paths in
perl_modulesto prevent module search path manipulation. - Limit module permissions: Perl module files should be owned by root and not writable by the NGINX user.
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.

