Envoy’s WebAssembly filter mechanism allows you to inject custom logic into the request/response lifecycle without recompiling Envoy itself.

Let’s see it in action. Imagine you want to add a simple header to every incoming request.

# envoy.yaml
admin:
  access_log_path: /tmp/envoy.access.log
  address:
    socket_address:
      address: 0.0.0.0
      port_value: 9901
static_resources:
  listeners:
  - name: http_listener
    address:
      socket_address:
        address: 0.0.0.0
        port_value: 8080
    filter_chains:
    - filters:
      - name: envoy.filters.network.http_connection_manager
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
          stat_prefix: ingress_http
          route_config:
            name: local_route
            virtual_hosts:
            - name: local_service
              domains: ["*"]
              routes:
              - match:
                  prefix: "/"
                route:
                  cluster: httpbin_service
              # This is where we add our Wasm filter
              typed_per_filter_config:
                envoy.filters.http.wasm:
                  "@type": type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
                  config:
                    vm:
                      runtime: envoy.wasm.runtime.v8 # Or envoy.wasm.runtime.null
                      # If using runtime.null, the code will be embedded directly.
                      # For other runtimes, you specify the root ID.
                      # vm_id: "my_wasm_vm"
                    # Code can be provided inline or via a ConfigMap/filesystem
                    code:
                      local:
                        filename: /etc/envoy/filters/add_header.wasm
                    # Specify the root filter configuration
                    root_id: "add_header_root"
                    # Optional: Specify filters for specific phases (e.g., request header, response header)
                    # phase: 1 # Envoy Wasm filter phases: 1 (headers), 2 (body), 3 (trailers)
          http_filters:
          - name: envoy.filters.http.wasm
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
              config:
                vm:
                  runtime: envoy.wasm.runtime.v8
                  # vm_id: "my_wasm_vm" # Must match the root_id if not using runtime.null
                code:
                  local:
                    filename: /etc/envoy/filters/add_header.wasm
                root_id: "add_header_root"
  clusters:
  - name: httpbin_service
    connect_timeout: 0.25s
    type: LOGICAL_DNS
    # Use the built-in httpbin service for testing
    # If running locally, you might use a local IP and port
    dns_lookup_family: V4_ONLY
    lb_policy: ROUND_ROBIN
    load_assignment:
      cluster_name: httpbin_service
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: httpbin.org
                port_value: 80

Here’s a simple add_header.wasm filter written in Rust and compiled to WebAssembly:

// src/lib.rs
use proxy_wasm_std::hostcalls;
use proxy_wasm_std::types::*;

#[no_mangle]
pub extern "C" fn _start() {
    proxy_wasm_std::set_log_level(LogLevel::Trace);
    proxy_wasm_std::register_plugin(|_context_id| -> Box<dyn Plugin> {
        Box::new(AddHeaderFilter {})
    });
}

struct AddHeaderFilter {}

impl Plugin for AddHeaderFilter {
    fn on_request_headers(&mut self, _num_headers: usize) -> Action {
        hostcalls::add_header("X-Custom-Header", "Hello-Wasm").unwrap();
        Action::Continue
    }
}

When Envoy starts with this configuration and the compiled add_header.wasm file at /etc/envoy/filters/add_header.wasm, and you send a request to http://localhost:8080/anything, you’ll see X-Custom-Header: Hello-Wasm in the output from httpbin.org.

The core problem WebAssembly filters solve is the desire to extend Envoy’s functionality with custom, dynamic logic without the operational burden of building and deploying a new Envoy binary. This is particularly useful for tasks like custom authentication, request/response transformation, rate limiting enforcement, or injecting request IDs.

Internally, Envoy uses a WebAssembly runtime (like Wasmtime, WasmEdge, or the V8 JavaScript engine compiled for Wasm) to execute your compiled .wasm module. The proxy-wasm SDK (available for Rust, C++, Go, and AssemblyScript) provides a standardized API that your Wasm module uses to interact with Envoy. When a wasm HTTP filter is configured, Envoy loads the Wasm VM, loads your compiled module into it, and then calls into your module at specific points in the HTTP request/response lifecycle (defined by phase in the config, or implicitly by the filter’s capabilities). Your Wasm code then uses the proxy-wasm API to inspect or modify the request/response.

The most surprising thing most people miss is how granularly you can control the Wasm execution flow. While you can simply return Action::Continue from a callback, you can also return Action::Pause and then resume processing later via host calls, or Action::Stop to terminate the request processing immediately. This allows for complex asynchronous operations or early termination based on dynamic conditions. For instance, you could pause a request, perform an external lookup using Envoy’s http_call host function, and then resume processing with the result.

The next concept to explore is how to implement more complex logic, such as dynamic routing or custom authentication, using Wasm filters.

Want structured learning?

Take the full Envoy course →