Skip to main content

NGINX / Server Setup

NGINX WebAssembly: Extend NGINX with Proxy-Wasm Filters

by ,


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.

You need to add custom logic to your NGINX reverse proxy — an authentication check that calls an external API, a rate limiter that shares state across workers, or a request transformer that rewrites headers. The problem is that NGINX was not designed to be easily extensible. Your options have been painful: write a C module (complex, unsafe, requires recompilation) or use Lua scripting through OpenResty (no sandboxing, vendor lock-in). NGINX WebAssembly support finally changes this.

With the NGINX WebAssembly module (ngx_wasm_module), you write filters in Rust, Go, C++, or AssemblyScript. These compile to WebAssembly bytecode and run at near-native speed inside a sandboxed runtime. A bug in your filter cannot crash the server. Better still, filters follow the Proxy-Wasm standard — the same compiled binary works across NGINX, Envoy, and other compatible proxies without modification.

What is Proxy-Wasm?

Proxy-Wasm is a set of binary specifications that standardize how WebAssembly extensions interact with network proxies. Originally developed for Envoy, Proxy-Wasm ensures that filters are “proxy-agnostic” — a filter compiled once can run on any compliant host, whether that is Envoy, NGINX, or Kong Gateway.

The specification defines two complementary parts:

  • Host ABI (Application Binary Interface): The low-level interface that the proxy exposes to WebAssembly modules. It provides functions for reading request headers, modifying responses, making HTTP calls, and accessing shared memory.
  • SDK Libraries: High-level frameworks that wrap the Host ABI into idiomatic language constructs. SDKs are available for Rust, Go (TinyGo), C++, and AssemblyScript.

You write your filter using the SDK in your preferred language, compile it to a .wasm binary, and load it into NGINX. No C code, no recompilation, no vendor lock-in.

How the NGINX WebAssembly Module Works

The NGINX WebAssembly module embeds the Wasmtime runtime directly into NGINX worker processes. Wasmtime, developed by the Bytecode Alliance, is one of the most mature WebAssembly runtimes available. The module integrates WebAssembly into the NGINX request pipeline through two mechanisms:

Direct Wasm Function Calls

The wasm_call directive invokes exported functions from a WebAssembly module at designated NGINX phases (rewrite, access, content, header_filter, body_filter, log). This is the simplest way to run WebAssembly code in NGINX.

Proxy-Wasm Filter Chains

The proxy_wasm directive attaches a Proxy-Wasm filter to the request processing chain. Filters implement callbacks like on_request_headers, on_response_body, and on_http_call_response. Multiple filters can be chained together, executing sequentially — similar to middleware in modern web frameworks.

Architecturally, the NGINX WebAssembly module:

  1. Loads WebAssembly modules during master process initialization for validation
  2. Re-loads them in each worker process after fork()
  3. Creates Wasm instances per worker, with configurable isolation levels
  4. Routes NGINX processing phases through the Proxy-Wasm filter chain
  5. Provides shared memory zones for inter-worker communication

Installation

The NGINX WebAssembly module is installed as a dynamic module package. It follows the same pattern as other NGINX dynamic modules — like the Zstd decompression module or the WAF module — from the GetPageSpeed repository.

RHEL, CentOS, AlmaLinux, Rocky Linux

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

After installation, load the module by adding the following to the top of /etc/nginx/nginx.conf:

load_module modules/ngx_wasmx_module.so;

Alternatively, include all installed module configuration files automatically:

include /usr/share/nginx/modules/*.conf;

Verify the module is loaded by testing the configuration:

nginx -t

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

Debian and Ubuntu

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

sudo apt-get update
sudo apt-get install nginx-module-wasm-wasmtime

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

Note: The NGINX WebAssembly module may have limited availability in APT repositories depending on your platform. Check the APT module page for current status.

Configuration

The NGINX WebAssembly module introduces directives across two main contexts: the top-level wasm{} block and the standard http{}, server{}, location{} hierarchy.

The wasm{} Block

The wasm{} block is a top-level directive (same level as events{} and http{}). It configures the WebAssembly runtime and loads Wasm modules.

Loading Modules

wasm {
    module my_filter /path/to/filter.wasm;
    module another_filter /path/to/another.wasm 'config=value';
}

The module directive loads a WebAssembly file and accepts three arguments:

  • name: A unique identifier for referring to this module elsewhere
  • path: Path to a .wasm (binary) or .wat (text format) file
  • config (optional): A configuration string passed to the filter’s on_vm_start callback

Runtime Configuration

The wasm{} block supports nested runtime-specific settings via the wasmtime{} sub-block:

wasm {
    compiler auto;
    backtraces on;

    wasmtime {
        flag epoch_interruption on;
        cache_config /etc/nginx/wasmtime-cache.toml;
    }
}
Directive Default Description
compiler auto WebAssembly compiler: auto or cranelift
backtraces off Enable detailed backtraces in error logs on Wasm traps
flag Runtime-specific configuration flags (context: wasmtime{})
cache_config Wasmtime compilation cache config in TOML format (context: wasmtime{})

DNS Resolution

Wasm filters that make HTTP dispatches need DNS resolution. Configure the global resolver in the wasm{} block:

wasm {
    resolver 1.1.1.1 ipv6=off;
    resolver_timeout 10s;
}
Directive Default Description
resolver 8.8.8.8 DNS resolver for Wasm HTTP dispatches
resolver_timeout 30s DNS resolution timeout

The resolver_add directive adds static host entries (similar to /etc/hosts). It works in both wasm{} and http{}/server{}/location{} contexts:

resolver_add 127.0.0.1 my-backend.local;

Socket Configuration

These directives control timeouts and buffers for TCP connections made by Wasm filters:

wasm {
    socket_connect_timeout 30s;
    socket_send_timeout 30s;
    socket_read_timeout 30s;
    socket_buffer_size 4k;
    socket_buffer_reuse on;
    socket_large_buffers 4 16k;
}
Directive Default Description
socket_connect_timeout 60s TCP connection timeout
socket_send_timeout 60s TCP send timeout
socket_read_timeout 60s TCP read timeout
socket_buffer_size 1024 Response payload buffer size
socket_buffer_reuse on Reuse buffers across connections
socket_large_buffers 4 8192 Number and size of large buffers for response headers

TLS Configuration

Configure certificate verification for outgoing TLS connections from Wasm filters:

wasm {
    tls_trusted_certificate /etc/pki/tls/certs/ca-bundle.crt;
    tls_verify_cert on;
    tls_verify_host on;
    tls_no_verify_warn off;
}
Directive Default Description
tls_trusted_certificate CA certificate bundle path in PEM format
tls_verify_cert off Verify server certificates against the trusted CA store
tls_verify_host off Verify certificate hostnames match the target
tls_no_verify_warn off Suppress TLS verification warning logs

Shared Memory

Shared memory zones let Wasm filters across different worker processes share data:

wasm {
    shm_kv my_cache 256k eviction=slru;
    shm_queue my_events 128k;

    metrics {
        slab_size 5m;
        max_metric_name_length 256;
    }
}
Directive Default Description
shm_kv Shared key/value store. Eviction: slru (default), lru, or none
shm_queue Shared queue for inter-process messaging
slab_size 5m Metrics storage memory (min: 3 × page size, context: metrics{})
max_metric_name_length 256 Max metric name length, min: 6 (context: metrics{})

HTTP Context Directives

These directives are available in http{}, server{}, and location{} blocks.

Proxy-Wasm Filters

The proxy_wasm directive attaches a filter to the request processing chain:

location /api {
    proxy_wasm my_auth_filter 'realm=api';
    proxy_wasm my_rate_limiter;
    proxy_wasm_isolation stream;

    proxy_pass http://backend;
}
Directive Default Description
proxy_wasm Attach a Proxy-Wasm filter with optional config string
proxy_wasm_isolation none Instance isolation: none, stream, or filter
proxy_wasm_request_headers_in_access off Run on_request_headers in access phase instead of rewrite

Isolation modes control how WebAssembly instances are shared:

  • none: All filters of the same module share one instance per worker. Lowest memory usage, best for stateless filters.
  • stream: Filters of the same module within one request share an instance. Good for request-scoped state.
  • filter: Each filter gets its own instance. Strongest isolation, highest memory usage.

Direct Wasm Calls

The wasm_call directive invokes a specific exported function at a given NGINX phase:

location / {
    wasm_call rewrite my_module my_rewrite_func;
    wasm_call log my_module my_log_func;
    proxy_pass http://backend;
}

It takes three arguments:

  • phase: One of rewrite, access, content, header_filter, body_filter, or log
  • module: Name of the loaded Wasm module (from the wasm{} block)
  • function: Name of the exported function to call

HTTP Socket Overrides

Per-location socket settings override the global wasm{} defaults:

Directive Default Description
wasm_socket_connect_timeout 60s Per-context connect timeout
wasm_socket_send_timeout 60s Per-context send timeout
wasm_socket_read_timeout 60s Per-context read timeout
wasm_socket_buffer_size 1024 Per-context buffer size
wasm_socket_buffer_reuse on Per-context buffer reuse toggle
wasm_socket_large_buffers 4 8192 Per-context large buffers for response headers
wasm_response_body_buffers 4 4096 Response body buffering for filter chains

Phase Postponement

These directives control when NGINX WebAssembly phase handlers run relative to other modules. Primarily useful in OpenResty builds:

Directive Default Description
wasm_postpone_rewrite off Defer Wasm rewrite handler (auto-enabled in OpenResty)
wasm_postpone_access off Defer Wasm access handler (auto-enabled in OpenResty)

Getting Started: Your First WebAssembly Filter

Here is a complete walkthrough to verify the NGINX WebAssembly module works.

Step 1: Create a Simple Wasm Module

Create a WebAssembly text file at /etc/nginx/hello.wat:

(module
  (func (export "say_hello"))
)

This defines a minimal Wasm module with one exported function. The module accepts both .wat (text) and .wasm (binary) formats.

Step 2: Configure NGINX

load_module modules/ngx_wasmx_module.so;

user nginx;
worker_processes 1;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

events { worker_connections 1024; }

wasm {
    module hello /etc/nginx/hello.wat;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    server {
        listen 80;

        location / {
            wasm_call log hello say_hello;
            return 200 "Hello from NGINX with WebAssembly!\n";
        }
    }
}

Step 3: Test and Reload

nginx -t
systemctl reload nginx
curl http://localhost/

You should see:

Hello from NGINX with WebAssembly!

This confirms the WebAssembly runtime is initialized and running inside NGINX.

Writing a Proxy-Wasm Filter in Rust

For production use, Proxy-Wasm filters are the preferred approach. Here is a Rust filter that adds a custom response header:

use proxy_wasm::traits::*;
use proxy_wasm::types::*;

proxy_wasm::main! {{
    proxy_wasm::set_log_level(LogLevel::Info);
    proxy_wasm::set_http_context(|_, _| -> Box<dyn HttpContext> {
        Box::new(AddHeader)
    });
}}

struct AddHeader;

impl Context for AddHeader {}

impl HttpContext for AddHeader {
    fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action {
        self.add_http_response_header("X-Powered-By", "WasmX");
        Action::Continue
    }
}

Compile it to WebAssembly:

cargo build --target wasm32-wasip1 --release

Then load the resulting .wasm binary in your NGINX configuration:

wasm {
    module add_header /etc/nginx/filters/add_header.wasm;
}

http {
    server {
        listen 80;

        location / {
            proxy_wasm add_header;
            proxy_pass http://backend;
        }
    }
}

Every response through this location will include X-Powered-By: WasmX — added by a sandboxed WebAssembly filter written in Rust.

Performance Considerations

Compilation Overhead

Wasmtime compiles WebAssembly to native code at load time using the Cranelift compiler. For large filters, this adds time to startup. Enable the compilation cache to persist artifacts across restarts:

wasm {
    wasmtime {
        cache_config /etc/nginx/wasmtime-cache.toml;
    }
}

Create a basic cache configuration at /etc/nginx/wasmtime-cache.toml:

[cache]
enabled = true
directory = "/var/cache/nginx/wasmtime"
cleanup-interval = "1h"
files-total-size-soft-limit = "512Mi"

Then create the cache directory:

mkdir -p /var/cache/nginx/wasmtime
chown nginx:nginx /var/cache/nginx/wasmtime

Memory Usage

Each WebAssembly instance consumes memory for its linear memory, stack, and compiled code. The isolation mode directly affects instance count:

  • none: One instance per module per worker. Minimal overhead — the best default.
  • stream: One instance per request. Use when filters store per-request state in Wasm globals.
  • filter: One instance per filter per request. Only needed when filters must not share any state.

Start with proxy_wasm_isolation none and increase only if your filter logic requires it.

Socket Buffer Tuning

If your filters make HTTP dispatches to external services, tune socket buffers for expected response sizes:

wasm {
    socket_buffer_size 8k;
    socket_large_buffers 8 32k;
}

Increase socket_large_buffers for responses with large headers.

Security Best Practices

Enable TLS Verification for Outgoing Connections

By default, TLS verification is disabled for Wasm filter connections. In production, always enable it:

wasm {
    tls_trusted_certificate /etc/pki/tls/certs/ca-bundle.crt;
    tls_verify_cert on;
    tls_verify_host on;
}

Size Shared Memory Appropriately

Oversized zones waste RAM. Undersized zones cause write failures:

wasm {
    shm_kv rate_data 64k eviction=lru;
}

The lru policy automatically evicts old entries when full, preventing hard failures.

The WebAssembly Sandboxing Advantage

WebAssembly provides inherent sandboxing. Wasm modules cannot access the filesystem, network, or host memory beyond their allocated linear memory. All external interactions go through the Proxy-Wasm ABI.

This contrasts with native C modules (full process access) and Lua scripts (unrestricted os and io access). If you already use NGINX security modules like the HTML Sanitize module for edge protection, the NGINX WebAssembly module provides an even stronger boundary for third-party code.

Enable Backtraces During Development

Enable Wasm backtraces for detailed error output when a filter traps:

wasm {
    backtraces on;
}

Wasmtime backtraces show function names, filenames, and line numbers. Disable in production to reduce log noise.

Troubleshooting

“unknown directive wasm”

The module is not loaded. Add load_module modules/ngx_wasmx_module.so; to the very top of nginx.conf, before all other blocks.

“module not loaded” or Failed to Load Module

The Wasm file is missing or unreadable. Verify the path and permissions:

ls -la /path/to/module.wasm
namei -l /path/to/module.wasm

Check validity with the wasmtime CLI (available from GetPageSpeed):

dnf install wasmtime
wasmtime compile /path/to/module.wasm

Filter Initialization Fails but NGINX Exits with Code 0

Proxy-Wasm filters start in worker processes after the master has already exited. Check the error log:

grep -i "wasm\|proxy_wasm" /var/log/nginx/error.log

Socket Timeouts in Proxy-Wasm Dispatches

Increase socket timeouts at the location level:

location /api {
    wasm_socket_connect_timeout 30s;
    wasm_socket_read_timeout 30s;
}

The dispatch_http_call() SDK method also accepts a per-call timeout.

Shared Memory Full

Increase the zone size or add an eviction policy:

wasm {
    shm_kv my_data 512k eviction=lru;
}

The minimum zone size is 15k. The default metrics slab_size of 5m holds about 20,000 counters with 64-char names and 4 workers.

Conclusion

The NGINX WebAssembly module brings a new extension model to NGINX. Write filters in Rust or Go, benefit from sandboxing, and deploy the same binaries across proxy platforms.

Whether you need authentication, rate limiting, request transformation, or observability — the NGINX WebAssembly module lets you build it without C code or recompilation.

The module is available as nginx-module-wasm-wasmtime from the GetPageSpeed repository. For source code and issues, visit the ngx_wasm_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

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.