Skip to main content

Your Gateway's Next Feature Doesn't Need an Envoy Fork: What kgateway's Rust Dynamic Modules Actually Buy a Self-Hosted PaaS

10 min readDora NodaDora Noda
Share
On this page

Every gateway ships with a set of built-in policies — auth, rate limiting, routing, prompt guards — and every platform team eventually hits the request none of them cover. Add a header based on a database lookup. Rewrite an AI agent's payload in flight. Enforce a per-tenant rule no off-the-shelf filter anticipates.

At that point the industry's oldest gateway question appears: do you fork the proxy, or do you live without the feature?

A May 2026 CNCF hands-on lab by kgateway contributor Michael Uzukwu gives the current best answer for Envoy-based gateways: neither. You compile your logic to a Rust shared library, load it into Envoy at runtime, and keep tracking upstream releases. Here is the fork-vs-extend decision in one table, with the verdict up front — then the honest accounting of what the mechanism costs.

The fork-vs-extend decision, in one table

MechanismRuns whereSpeedPowerPrice you pay
Native C++ filter (fork)In-processNativeTotalRebase onto every upstream release, forever
Lua filterIn-processInterpretedHeaders + small logicLittle; the simplest option that works
Wasm filterIn-process sandboxNear-nativeBroadVM overhead + Wasm idiosyncrasies
ext_proc (external processor)Out-of-process gRPC serviceExtra network hop per callAny language, full I/OLatency + another service to operate
Rust dynamic moduleIn-process, loaded at runtimeNativeBroadNo sandbox; ABI-coupled to the Envoy build

The verdict: if your logic must run at proxy speed on every request and you refuse to maintain a forked Envoy, the dynamic module is the only row with native speed and no recompile. Everything else in this post is the fine print behind that verdict — what the mechanism looks like in practice, where each alternative still wins, and the three catches the headline doesn't mention.

What the May 2026 CNCF lab actually builds

The lab, "Extending AI gateways with Rust: Custom transformations in kgateway", is a 30–45 minute exercise that runs entirely on a laptop: kind cluster, no cloud bill, no API keys. The request path it builds is:

text
curl → kgateway-proxy (Envoy) → Rust module (.so) → httpbun (mock LLM) → response

Concretely, you write transformation logic in Rust, compile it to a .so shared library, bake that library into the proxy's Docker image, deploy it under kgateway, and wire it through a TrafficPolicy — the Gateway-API-adjacent policy object kgateway uses for per-route behavior. The proof that your code is executing inside the proxy is a visible header on every response: X-Custom-Transformed: true, set by a single line of Rust. The backend is httpbun standing in for an LLM endpoint, so the whole loop exercises the exact shape of an AI-gateway request without spending a token.

Two context facts matter before you go further. First, kgateway is the Envoy-based Gateway API implementation Solo.io donated to the CNCF — announced at KubeCon North America 2024 in Salt Lake City, accepted into the Sandbox in March 2025 — carrying the production history of Gloo Gateway with it. Second, the lab was built against kgateway v2.3.0-rc.1, and v2.3 introduced breaking changes to the Rust dynamic module system, with a migration guide in the repo for anyone on the older layout. Keep that second fact in mind; it becomes catch number one.

The full extension ladder, honestly scored

It helps to see the dynamic module as the newest rung on a ladder Envoy operators have climbed for years, because every older rung still wins some workload.

The fork: a native C++ filter compiled into Envoy. Total power, total ownership. You can touch anything in the request path at full speed. The price is the upgrade tax: every upstream Envoy or gateway release means rebasing your patch, rebuilding, retesting, and re-shipping a proxy image only you run. For a two-person platform team, that tax compounds quarterly until the fork is the reason upgrades stop happening. The whole point of every other row in the table is to avoid this fate.

Lua: the inline script. Envoy's Lua HTTP filter runs scripts directly inside the proxy with no compilation step — envoy_on_request, envoy_on_response, done. For header munging and small conditional logic it is the cheapest correct answer, and it ships in every Envoy. It loses on compute-heavy work (it's interpreted) and on anything needing libraries or type safety, but most gateway customizations never outgrow it. If your requirement fits in fifty lines of Lua, stop here.

Wasm: the sandboxed module. Envoy embeds a Wasm runtime precisely so teams can extend the proxy without recompiling or forking it: write in Rust, Go, or C++, push the module, load it dynamically — even into running proxies. The sandbox is the selling point: a buggy or malicious filter can't crash Envoy or touch unauthorized memory, which is what makes Wasm viable for multi-tenant fleets where different teams ship their own extensions. The price is VM overhead on the hot path and a long tail of Wasm idiosyncrasies (ABI quirks, SDK gaps, debugging pain) that have kept it from fully displacing simpler options.

ext_proc: the sidecar service. The external-processor API streams request and response data — including bodies — over gRPC to a service you write in any language, with full I/O access: databases, internal APIs, whatever the logic needs. Nothing matches it for "add a header based on a database lookup." The price is architectural: an extra network hop on every processed request plus another stateful-ish service to deploy, scale, monitor, and keep from becoming the latency tail of your gateway.

Dynamic modules: native code, loaded at runtime. This is the mechanism the CNCF lab exercises. Envoy's dynamic-modules filter loads a shared object into the proxy process at runtime — no recompile, no fork, no VM, no sidecar hop. The officially supported language is Rust (the interface itself is a plain C ABI header, so anything that builds a .so can play; the Envoy examples repo also carries a Go SDK). You get in-process native speed with a real language toolchain. The price, in one sentence: there is no sandbox. A null dereference in your module doesn't return an error to the filter chain — it takes down the Envoy worker. That single fact redraws the whole comparison: dynamic modules trade Wasm's safety boundary for raw speed and simplicity, which is exactly right for first-party platform logic you test like proxy code, and exactly wrong for untrusted tenant-supplied extensions.

The three catches nobody puts in the headline

Catch 1: the ABI coupling is real, and v2.3 just proved it. The lab's own note says it plainly: kgateway v2.3 introduced breaking changes to the Rust dynamic module system, and the custom-header logic had to be ported to the new structure. A .so loaded into a process is coupled to that process's ABI in a way a sidecar never is. Upgrading Envoy or kgateway can mean rebuilding — sometimes rewriting — your module. This is still dramatically cheaper than maintaining a fork, but it is not "write once, load forever." Budget for the migration guide on major upgrades.

Catch 2: you own memory safety now. No sandbox means your Rust (or C, or Go) runs with the proxy's privileges and fate. Safe Rust helps enormously here — which is presumably why it's the officially supported language — but unsafe blocks, FFI boundaries, and panics across the ABI line are all live risks. Test a dynamic module the way you'd test a proxy patch: fuzz the inputs, fault-inject the failure paths, and canary it before it touches tenant traffic. If that sentence made you flinch, Wasm or ext_proc is your mechanism.

Catch 3: you still own a build pipeline. "No fork" doesn't mean "no build." Somebody maintains the Rust toolchain, rebuilds the .so per Envoy/kgateway version, bakes it into the proxy image, versions that image, and rolls it out. The lab hand-waves this with a local Docker build; production needs CI, artifact signing, and a rollout strategy for the proxy fleet. Compare honestly against ext_proc, where the extension deploys as an ordinary service with ordinary rollbacks.

None of these catches kills the mechanism. Together they define its shape: dynamic modules are for tested, first-party, performance-sensitive gateway logic owned by the platform team — not for tenant plugins, not for logic that needs database I/O per request, not for teams without CI for a native artifact.

What this changes for a self-hosted PaaS gateway layer

Map this onto a self-hosted platform running its gateway on owned machines, and the value lands in a specific place: payload-level control without a forked proxy image drifting from upstream releases.

Consider the concrete roadmap item the space keeps circling: agent-bound traffic needs per-tenant treatment at the gateway. Inject tenant auth into requests headed for an LLM provider. Enforce per-tenant rate limits on token spend before the request leaves the fleet.

Scrub or rewrite fields in an agent's payload in flight. Shape MCP or agent-to-agent traffic differently from ordinary HTTPS. Before a supported extension point, each of these was a fork-or-sidecar decision — maintain a patched Envoy build that falls out of sync with every kgateway release, or stand up a separate proxy layer (hop, service, on-call surface) just for agent traffic.

The dynamic module collapses that to a versioned .so riding the stock proxy image: the platform team writes the tenant logic once, in Rust, tested like proxy code, and the gateway upgrade path stays "pull the new upstream image, rebuild the module, roll." The per-tenant auth injection and rate-limit logic live exactly where they belong — in the request path, at proxy speed — without either a fork or a second proxy tier to operate.

One scoping note before you build: know which data plane you're extending. Starting with kgateway 2.3.0, the control plane for agentgateway — Solo.io's ground-up Rust data plane for AI and agentic traffic — moved to its own repo, leaving kgateway focused on being a stable, Envoy-powered API gateway. If your roadmap's agent traffic will eventually ride agentgateway's LLM/MCP/A2A handling, prototype the extension against the data plane you'll actually run in production, not just the one the lab uses. The fork-vs-extend math is the same either way; only the target moves.

The bottom line

The CNCF lab's real contribution isn't the header — it's the worked proof that the fifth row of the extension table is usable today by a team with a laptop and an afternoon. Lua still wins small logic, Wasm still wins untrusted code, ext_proc still wins database-backed decisions, and the fork still wins nothing except regret. But for first-party, per-request, proxy-speed logic on a gateway you operate yourself, a Rust dynamic module is now the default answer, with three known catches and a migration guide. That's a better deal than any gateway extension story has offered in years.

Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Star the repo on GitHub or deploy your first app today.

Related articles

Give your agents a chain backend

Autonomous agents hit RPC endpoints very differently than people do. See what bex router handles on their behalf.

Read the agents guide