Site icon GetPageSpeed

NGINX Echo Module: Shell-Style Scripting in Your Config

NGINX Echo Module: Shell-Style Scripting in Your Config

The NGINX echo module brings shell-style scripting capabilities directly into your NGINX configuration. Instead of relying on external CGI scripts or upstream application servers for simple responses, this module lets you output text, measure request timing, issue subrequests, and iterate over data — all from within your NGINX config file.

Whether you need to debug request routing, build lightweight health-check endpoints, prototype API responses, or test module interactions, the NGINX echo module is the right tool. This guide covers installation, every directive and variable, practical examples you can deploy today, and production best practices.

What Is the NGINX Echo Module?

The NGINX echo module (ngx_http_echo_module) is developed by the OpenResty project and is available as version 0.64. It transforms NGINX configuration blocks into simple scripts that execute sequentially, much like a shell script. Each directive in a location block runs in order, and variables are interpolated automatically.

The module operates in two modes:

This dual-mode design makes it useful for both standalone endpoints and augmenting existing proxy configurations.

Installing the NGINX Echo Module

RHEL, CentOS, AlmaLinux, Rocky Linux

Install the module from the GetPageSpeed extras repository:

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

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

load_module modules/ngx_http_echo_module.so;

Debian and Ubuntu

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

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

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

For the full list of available platforms and versions, see the RPM module page or APT module page.

Verify the Installation

After installing, verify that NGINX accepts the configuration:

sudo nginx -t

If you see “syntax is ok” and “test is successful,” the module is loaded and ready to use.

NGINX Echo Module Directives Reference

The module provides 19 directives. All directives are valid in location and if blocks.

echo

The core directive. It outputs its arguments joined by spaces, followed by a trailing newline.

location /hello {
    echo "Hello from NGINX!";
}

Use the -n flag to suppress the trailing newline:

location /greeting {
    echo -n "Hello, ";
    echo "World!";
}

This produces Hello, World! on a single line. Variable interpolation works automatically:

location /info {
    echo "Your IP: $remote_addr";
    echo "Host: $host";
    echo "Method: $request_method";
}

echo_status

Sets the HTTP response status code for the response. The default status code is 200.

location /not-found {
    echo_status 404;
    echo "The requested resource was not found.";
}
location /created {
    echo_status 201;
    echo "Resource created successfully.";
}

echo_sleep

Introduces a non-blocking delay. This is critical: the module uses NGINX’s event loop, so a sleeping request does not block the worker process. Other requests continue to be served normally.

location /delayed {
    echo "Processing started...";
    echo_flush;
    echo_sleep 2;
    echo "Done after 2 seconds.";
}

The sleep resolution is 0.001 seconds, making it useful for simulating latency in test environments:

location /slow-api {
    echo_sleep 0.5;
    echo '{"status": "ok", "latency": "simulated"}';
}

echo_blocking_sleep

A blocking version of sleep that actually blocks the NGINX worker process. This should never be used in production because it prevents the worker from handling any other requests during the sleep period.

# WARNING: Blocks the entire worker process
location /blocking {
    echo_blocking_sleep 1;
    echo "This blocked a worker for 1 second.";
}

echo_flush

Forces the output buffer to flush immediately. This is essential when combining echo with echo_sleep to create streaming responses:

location /stream {
    echo "Step 1: Starting...";
    echo_flush;
    echo_sleep 1;
    echo "Step 2: Processing...";
    echo_flush;
    echo_sleep 1;
    echo "Step 3: Complete.";
}

Without echo_flush, NGINX may buffer the output and deliver it all at once when the response finishes.

echo_duplicate

Repeats a string a specified number of times without adding a trailing newline:

location /separator {
    echo_duplicate 80 "=";
    echo;
    echo "Report Title";
    echo_duplicate 80 "=";
    echo;
}

For large numbers, you can use underscore notation for readability:

location /large-response {
    echo_duplicate 1_000_000 "x";
}

This generates a 1 MB response body, which is useful for bandwidth and buffer testing.

echo_reset_timer

The echo_reset_timer directive resets the internal timer to the current moment. The $echo_timer_elapsed variable then shows the time elapsed since that reset, with millisecond precision:

location /timed {
    echo_reset_timer;
    echo "Start";
    echo_sleep 1.5;
    echo "Elapsed: $echo_timer_elapsed seconds";
}

Output:

Start
Elapsed: 1.501 seconds

This combination is invaluable for measuring the execution time of subrequests, backend responses, or sleep delays.

echo_read_request_body

The echo_read_request_body directive explicitly loads the HTTP request body into memory. After calling it, $echo_request_body (or the built-in $request_body) contains the body content:

location /echo-back {
    echo_read_request_body;
    echo "You sent: $echo_request_body";
}

Test with:

curl -X POST -d '{"key": "value"}' http://localhost/echo-back

Output:

You sent: {"key": "value"}

echo_location_async and echo_location

These directives issue subrequests to other NGINX locations. The _async variant runs subrequests in parallel, while the version without _async runs them sequentially.

Parallel subrequests — all locations are fetched concurrently:

location /dashboard {
    echo_reset_timer;
    echo_location_async /api/users;
    echo_location_async /api/stats;
    echo "Fetched in $echo_timer_elapsed seconds";
}

location /api/users {
    echo_sleep 0.2;
    echo '{"users": 42}';
}

location /api/stats {
    echo_sleep 0.3;
    echo '{"requests": 1500}';
}

The total time is approximately 0.3 seconds (the longest subrequest), not 0.5 seconds.

Sequential subrequests — each waits for the previous one to complete:

location /sequential {
    echo_location /step1;
    echo_location /step2;
}

Both variants accept an optional query string argument:

location /search {
    echo_location_async /api/search?q=nginx;
}

echo_subrequest_async and echo_subrequest

These extend the subrequest directives with support for custom HTTP methods and request bodies:

location /api-test {
    echo_subrequest_async POST /api/data -b '{"action": "create"}';
}

Options include:

echo_foreach_split and echo_end

Create loops by splitting a string on a delimiter:

location /fruits {
    echo_foreach_split "," "apple,banana,cherry,mango";
    echo "Fruit: $echo_it";
    echo_end;
}

Output:

Fruit: apple
Fruit: banana
Fruit: cherry
Fruit: mango

This is powerful for processing query parameters or comma-separated values. You can also use NGINX variables:

location /merge-js {
    default_type application/javascript;
    echo_foreach_split "&" $query_string;
    echo "/* === $echo_it === */";
    echo_location_async $echo_it;
    echo_end;
}

echo_exec

Performs an internal redirect to a named or unnamed location, similar to how the rewrite directive’s last flag works:

location /old-path {
    echo_exec /new-path;
}

location /new-path {
    echo "You have been redirected internally.";
}

Named locations are also supported:

location /dispatch {
    echo_exec @handler;
}

location @handler {
    echo "Handled by named location.";
}

echo_abort_parent

Aborts the parent (main) request. This directive is useful in subrequest contexts where you want to halt processing entirely.

echo_before_body and echo_after_body

These filter directives prepend or append content to a response generated by another handler. They work with proxy_pass, fastcgi_pass, or any other content handler:

location /wrapped {
    echo_before_body "<!-- Response from backend -->";
    echo_after_body "<!-- End of response -->";
    proxy_pass http://backend;
}

They can also be used to add debug information to proxied responses:

location /debug-proxy {
    echo_before_body "<!-- Debug: served at $date_local -->";
    echo_after_body "<!-- Debug: upstream response time $upstream_response_time -->";
    proxy_pass http://127.0.0.1:8080;
}

NGINX Echo Module Variables

The module exports 10 variables that provide request metadata and runtime information.

Variable Description
$echo_timer_elapsed Seconds elapsed since request start or last echo_reset_timer (e.g., 1.503)
$echo_request_method HTTP method of the current request or subrequest (GET, POST, etc.)
$echo_client_request_method HTTP method of the original client request (always the main request)
$echo_request_uri URI of the current request (non-cacheable, always fresh)
$echo_cacheable_request_uri URI of the current request (cacheable version)
$echo_request_body Raw request body content
$echo_client_request_headers Raw client request headers including the request line
$echo_it Current iteration value inside an echo_foreach_split loop
$echo_incr Auto-incrementing counter (starts at 1, increments on each access)
$echo_response_status HTTP response status code of the current request

The $echo_incr Counter

The $echo_incr variable is unique: it auto-increments each time it is accessed within the same request:

location /counter {
    echo "Line $echo_incr";
    echo "Line $echo_incr";
    echo "Line $echo_incr";
}

Output:

Line 1
Line 2
Line 3

The $echo_client_request_headers Variable

This variable exposes the raw HTTP request headers from the client:

location /show-headers {
    echo "=== Your Request Headers ===";
    echo $echo_client_request_headers;
}

Note: This variable is not available in HTTP/2 or HTTP/3 connections due to NGINX internal limitations. The headers are only captured for HTTP/1.x requests.

Practical Use Cases for the NGINX Echo Module

Health Check Endpoint

Create a lightweight health-check endpoint that does not require any backend application:

location /health {
    echo_status 200;
    default_type application/json;
    echo '{"status": "healthy", "server": "$hostname"}';
}

Request Debugging and Inspection

When troubleshooting NGINX routing issues or upstream behavior, create a debug endpoint that mirrors back everything the server received:

location /debug {
    default_type text/plain;
    echo "=== Request Info ===";
    echo "Method: $request_method";
    echo "URI: $request_uri";
    echo "Host: $host";
    echo "Remote Addr: $remote_addr";
    echo "Server Protocol: $server_protocol";
    echo "";
    echo "=== Headers ===";
    echo $echo_client_request_headers;
}

Mock REST API for Frontend Development

When your backend is not ready yet, use the echo module to mock entire API endpoints. Frontend developers can start building immediately while the real API is under development:

location /api/v1/users {
    default_type application/json;
    echo '[{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]';
}

location /api/v1/users/1 {
    default_type application/json;
    echo '{"id": 1, "name": "Alice", "email": "alice@example.com"}';
}

This approach also works well for integration testing where you want deterministic API responses.

Simulating Slow Backends

Test how your application handles slow responses without modifying the actual backend:

location /slow-response {
    echo_reset_timer;
    echo_sleep 3;
    default_type application/json;
    echo '{"message": "delayed response", "delay": "$echo_timer_elapsed"}';
}

This is especially useful for testing timeout configurations in reverse proxy setups and load balancing scenarios.

Server-Sent Events (SSE) Simulation

Simulate a Server-Sent Events stream directly from NGINX, which is useful for testing EventSource clients without a backend:

location /events {
    default_type text/event-stream;
    echo -n "data: {\"time\": \"$date_local\", \"count\": 1}";
    echo;
    echo;
    echo_flush;
    echo_sleep 1;
    echo -n "data: {\"time\": \"$date_local\", \"count\": 2}";
    echo;
    echo;
    echo_flush;
    echo_sleep 1;
    echo -n "data: {\"time\": \"$date_local\", \"count\": 3}";
    echo;
    echo;
    echo_flush;
}

Each event follows the SSE format with data: prefix and double newline separators. The echo_flush directives ensure events are sent immediately instead of being buffered.

Maintenance Page

Serve a maintenance page with the correct 503 status code without needing a backend application:

location / {
    echo_status 503;
    default_type text/html;
    echo '<html><body>';
    echo '<h1>Scheduled Maintenance</h1>';
    echo '<p>We are performing upgrades. Check back soon.</p>';
    echo '</body></html>';
}

Webhook Receiver for Debugging

Capture and display incoming webhook payloads, which is invaluable when setting up integrations with services like GitHub, Stripe, or Slack:

location /webhook {
    echo_read_request_body;
    default_type text/plain;
    echo "=== Webhook Received ===";
    echo "Time: $date_local";
    echo "Method: $request_method";
    echo "Content-Type: $content_type";
    echo "Body:";
    echo $echo_request_body;
}

Test with:

curl -X POST -H "Content-Type: application/json" \
  -d '{"event": "push", "repo": "nginx"}' \
  http://localhost/webhook

CORS Preflight Handler

Combine the echo module with headers-more to handle CORS preflight requests cleanly:

location /api/endpoint {
    if ($request_method = OPTIONS) {
        more_set_headers "Access-Control-Allow-Origin: *";
        more_set_headers "Access-Control-Allow-Methods: GET, POST, PUT, DELETE";
        more_set_headers "Access-Control-Allow-Headers: Content-Type, Authorization";
        echo_status 204;
        echo;
    }
    default_type application/json;
    echo '{"message": "CORS-enabled endpoint"}';
}

The OPTIONS request returns a 204 with proper CORS headers, while GET/POST requests return the actual response.

Connection and Routing Diagnostics

When running NGINX behind a load balancer or CDN, verify how requests are being routed:

location /diag {
    default_type text/plain;
    echo "=== Server Diagnostics ===";
    echo "Server: $server_name:$server_port";
    echo "Client: $remote_addr:$remote_port";
    echo "Protocol: $server_protocol";
    echo "Request: $request_method $request_uri";
    echo "Host header: $http_host";
    echo "X-Forwarded-For: $http_x_forwarded_for";
    echo "X-Real-IP: $http_x_real_ip";
}

This endpoint reveals exactly which server block handled the request and what headers the load balancer is passing through.

Generating Test Data

Use echo_duplicate to generate responses of a specific size, which is useful for testing compression, buffer tuning, or bandwidth limits:

location /1mb {
    echo_duplicate 1_048_576 "A";
}

location /10kb {
    echo_duplicate 10_240 "B";
}

Combining Multiple Backend Responses

Aggregate responses from multiple internal locations into a single response, similar to Edge Side Includes (ESI):

location /page {
    echo_location_async /header;
    echo_location_async /content;
    echo_location_async /footer;
}

location /header {
    echo "<header>Site Header</header>";
}

location /content {
    proxy_pass http://app-backend;
}

location /footer {
    echo "<footer>Copyright 2026</footer>";
}

Measuring Upstream Response Time

Precisely measure how long subrequests or upstream backends take to respond:

location /benchmark {
    echo_reset_timer;
    echo_location /backend;
    echo "Backend responded in $echo_timer_elapsed seconds";
}

location /backend {
    proxy_pass http://127.0.0.1:8080;
}

Dynamic JSON Error Pages

Replace static error pages with dynamic JSON responses that include useful context for API clients:

location /error-test {
    echo_status 503;
    default_type application/json;
    echo '{"error": "service_unavailable", "server": "$hostname", "timestamp": "$date_local"}';
}

Chunked Transfer Encoding Test

Verify that your reverse proxy or CDN correctly handles chunked transfer encoding by streaming content progressively:

location /chunked {
    default_type text/plain;
    echo "Chunk 1: Starting transfer";
    echo_flush;
    echo_sleep 0.5;
    echo "Chunk 2: Data processing";
    echo_flush;
    echo_sleep 0.5;
    echo "Chunk 3: Transfer complete";
    echo_flush;
}

This helps identify whether intermediate proxies are buffering responses or passing them through immediately.

Performance Considerations

The NGINX echo module is lightweight and designed to work within NGINX’s event-driven architecture. However, keep these points in mind:

Non-blocking sleep: The echo_sleep directive uses NGINX’s timer system and does not consume a worker thread during the delay. Therefore, thousands of sleeping requests can coexist without impacting performance. However, each sleeping request does consume a connection slot.

Subrequest limits: NGINX imposes a default limit of 50 subrequests per main request. If you use echo_location_async or echo_subrequest_async heavily, you may hit this limit. Additionally, keep subrequest chains shallow to avoid excessive memory consumption.

Memory usage: The echo_duplicate directive with very large counts allocates memory proportional to the output size. For example, echo_duplicate 100_000_000 "x" creates a 100 MB response in memory. Use chunked responses or streaming approaches for extremely large outputs.

Content type: The module respects the default_type directive. Set it appropriately for your use case (e.g., application/json for API endpoints, text/plain for debugging).

Security Best Practices

While the NGINX echo module is primarily a debugging and utility tool, it is important to follow security best practices when deploying it.

Restrict Access to Debug Endpoints

Never expose debug endpoints publicly. Use allow and deny directives or NGINX authentication to limit access:

location /debug {
    allow 10.0.0.0/8;
    allow 192.168.0.0/16;
    deny all;

    echo "Debug info: $request_uri";
    echo $echo_client_request_headers;
}

Avoid Echoing Sensitive Data

Do not use echo directives to output variables that may contain sensitive information such as cookies, authorization headers, or internal IP addresses to untrusted clients.

Disable in Production (If Not Needed)

If the module is only needed for development or testing, remove the load_module directive in production configurations. Alternatively, keep debug locations behind authentication using JWT or TOTP modules.

Troubleshooting

“unknown directive echo”

This error means the module is not loaded. Add load_module modules/ngx_http_echo_module.so; to the top of your nginx.conf file, before any events or http blocks.

Output Not Appearing Immediately

If output arrives all at once instead of streaming, add echo_flush after each echo statement. NGINX buffers output by default.

Subrequest Returns Empty

If echo_location or echo_location_async returns empty content, verify that the target location exists and produces output. Moreover, internal locations must be defined in the same server block.

$echo_client_request_headers Is Empty

This variable does not work with HTTP/2 or HTTP/3. NGINX does not preserve the raw HTTP/1.x header format for multiplexed protocols. Test with HTTP/1.1:

curl --http1.1 http://localhost/show-headers

echo_foreach_split Produces No Output

Ensure you have a matching echo_end directive. The echo_foreach_split and echo_end must be in the same location block, and nested loops are not supported.

Conclusion

The NGINX echo module transforms NGINX into a versatile scripting environment for debugging, testing, and building lightweight endpoints. With 19 directives and 10 variables, it provides fine-grained control over request handling, response generation, and timing measurement — all without leaving your NGINX configuration.

From health checks and mock APIs to SSE simulation and webhook debugging, this module is an essential addition to any NGINX administrator’s toolkit. Install it from the GetPageSpeed repository and start building smarter NGINX configurations today.

For the full source code and issue tracking, visit the echo-nginx-module GitHub repository.

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