WebAssembly backend plugins
WebAssembly backend plugins run a plugin backend compiled to WebAssembly (WASM) in-process, inside a sandboxed virtual machine, instead of as a separate OS process that Grafana talks to over gRPC.
Caution
The WASM backend driver is experimental. It supports a reduced set of backend calls, and the host ABI described on this page isn’t stable yet.
When to use WebAssembly plugins
Use a WASM backend when you want a backend plugin that’s portable and cheap to isolate:
- One binary for every platform. A WASM module runs unchanged on every OS and CPU architecture Grafana supports, so you don’t build and ship a binary per platform.
- No CGO. The runtime is pure Go, so no C toolchain or native compilation step is required to build or run Grafana with this driver.
- In-process execution. There’s no separate process to spawn, so no process supervision, port allocation, or inter-process socket.
- Capability-based isolation. The module starts with no network, filesystem, or environment access, and reaches the outside world only through the
host_http_fetchandhost_loghost functions, subject to an allow-list.
Choose the standard gRPC backend plugin when you need streaming calls, metrics collection, chunked queries, or admission and conversion handlers, which the WASM driver doesn’t support yet.
How Grafana selects the backend driver
Grafana builds a chain of backend factory providers for every plugin. It asks each provider in turn for a factory and uses the first one that returns a factory. For a WASM plugin, the chain resolves like this:
- Core plugins, such as the built-in data sources.
- The WASM provider, which claims the plugin only if it declares itself as WASM.
- The default gRPC provider, which handles everything else.
The WASM provider claims a plugin when either condition holds:
plugin.jsonsetsbackendRuntimeto"wasm".- The plugin’s executable path ends in
.wasm, which covers plugins that don’t setbackendRuntimebut ship a WASM binary.
If neither condition holds, the WASM provider returns nothing and the chain continues to the gRPC provider.
Configure a WebAssembly plugin
Declare the plugin as WASM in plugin.json and point wasmFilePath at the module.
{
"id": "myorg-wasm-datasource",
"type": "datasource",
"backend": true,
"executable": "gpx_myorg_wasm_datasource",
"backendRuntime": "wasm",
"wasmFilePath": "module/main.wasm",
"wasmSandbox": {
"allowedOutboundHosts": ["api.example.com", "cache.example.com:8443"],
"allowedEnvVars": ["MYORG_API_TOKEN"],
"allowedReadPaths": ["/etc/myorg/certs:/certs"]
}
}
The following keys control the WASM backend:
backend: Set totrueso Grafana loads a backend for the plugin.backendRuntime: Set to"wasm"to select the WASM driver explicitly.wasmFilePath: Path to the WASM module, resolved relative to the plugin directory. When you omit it, Grafana uses the regularexecutablepath instead.wasmSandbox: Optional sandbox allow-lists the plugin asks for. See Sandbox allow-lists.
For the full plugin manifest reference, refer to the plugin schema.
Sandbox and isolation
A WASM plugin runs inside a wazero virtual machine. It has no filesystem, no sockets, and no environment variables unless Grafana explicitly grants them, and it can’t affect anything outside its own memory except through the host functions.
Sandbox allow-lists
Every capability a guest module may use is granted through an allow-list. Grafana builds those lists from three sources and merges them:
- The
wasmSandboxblock ofplugin.json. Honoured only when the plugin signature isvalidorinternal. The manifest and its signature are covered by the same signed hash, so a valid signature vouches for the grants as well; an unsigned or tampered manifest can’t widen its own sandbox. [plugins.wasm_sandbox.<plugin id>]. Entries for one plugin, set by an administrator.[plugins.wasm_sandbox]. Defaults applied to every WASM plugin.
The lists are additive: a source can widen what another one granted, but not narrow it. To keep the allow-lists purely administrator-defined, set ignore_manifest_grants = true in [plugins.wasm_sandbox].
[plugins.wasm_sandbox]
allowed_outbound_hosts = api.example.com
allowed_env_vars = SHARED_TOKEN
allowed_read_paths = /etc/ssl:/certs
[plugins.wasm_sandbox.grafana-glaber-datasource]
allowed_outbound_hosts = glaber:8080
allowed_env_vars = GLABER_TOKEN
Lists use the usual Grafana syntax: comma-separated values, optionally quoted, or a JSON array.
Guest output on standard output and standard error, including panic messages, is captured and written to the Grafana server log at debug level.
Guest ABI
A WASM backend module talks to Grafana through a small set of exported functions and imported host functions. Payloads are protobuf-encoded pluginv2 messages, the same wire format the gRPC driver uses, so a guest can reuse the Grafana plugin SDK’s proto types.
Exports
A module exports only the functions it supports. Any missing export is treated the same as a gRPC plugin that didn’t register the corresponding service, and the call fails with plugins.ErrMethodNotImplemented.
alloc(size uint32) uint32andfree(ptr uint32): Allocate and release guest memory. The host never writes to an address it picks itself; it always asks the guest to allocate a buffer first.wasm_query_data: Handle apluginv2.QueryDataRequestand return apluginv2.QueryDataResponse.wasm_check_health: Handle apluginv2.CheckHealthRequestand return apluginv2.CheckHealthResponse.wasm_call_resource: Handle apluginv2.CallResourceRequestand return apluginv2.CallResourceResponse.
Every exported function takes a single uint64 argument and returns a single uint64 result. Buffers are addressed as a packed pointer (ptr<<32 | len), which keeps the ABI to one parameter and one return value.
Imports
The host module grafana_host exposes the only ways a guest can reach outside its own memory:
host_http_fetch: Perform an outbound HTTP request. The guest passes a packed pointer to a JSON payload withmethod,url, optionalheaders, and optionalbody. The host validates the destination against the sandbox allow-list, performs the request with Grafana’s HTTP client, and writes back a JSON payload withstatus,headers,body, and anerrorfield. OnlyhttpandhttpsURLs are accepted, and the response body is capped in size.host_log: Forward a log line to the Grafana server log at debug level.
Build the module
Build the guest as a c-shared module for the wasip1 target, so the Go runtime exports _initialize rather than _start. Grafana starts the module through _initialize and returns control to the host without terminating the guest, which is why an executable-style _start module doesn’t work.
GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o module/main.wasm ./cmd/plugin
Outbound network access
The sandbox denies all outbound requests by default. A host_http_fetch call succeeds only when the destination host matches one of these:
- Explicit allow-list. Hosts from
allowedOutboundHostsinplugin.json(signed plugins only) and from[plugins.wasm_sandbox]in the configuration, compared case-insensitively. See Sandbox allow-lists. - The data source URL. For a data source plugin, the host of the data source instance’s configured URL is always reachable, because Grafana derives it from admin-controlled instance settings, not from the guest. A host without a port doesn’t match the same host with a port, and vice versa.
A blocked request fails with an error that names the host, and Grafana logs a warning.
Module compilation and the cache
wazero doesn’t interpret the bytecode as it runs; it compiles the WASM module to machine code before the first call. For a large module (a Go backend compiled to WASM is on the order of 30 MB with tens of thousands of functions) that compilation takes tens of seconds and happens on every Grafana process start. File size isn’t the main factor: the cost is driven by the number of functions and the amount of code, not by the bytes on disk.
To avoid recompiling the same module over and over, Grafana enables wazero’s on-disk compilation cache under the data directory (<data>/wasm-compilation-cache, see [paths] data in the configuration). The cache:
- Is keyed by module content and the wazero version, so upgrading either just adds a new entry instead of reusing a stale one.
- Is shared across all WASM plugins: entries for different modules don’t collide.
- Is safe for multiple processes: entries are written via a temporary file and an atomic rename, so concurrent Grafana starts won’t corrupt it.
The first start after a clean install or a module change is still slow โ that’s expected. Every later start reads the compiled result from disk and takes under a second. If the cache directory isn’t writable, Grafana logs a warning and keeps going, compiling without the cache.
The cache can be deleted at any time; Grafana recreates it on the next start.
Limitations
- Three calls only. The driver implements
QueryData,CheckHealth, andCallResource. Every other backend method, including metrics collection, chunked queries, streaming, admission, and conversion, returnsplugins.ErrMethodNotImplemented. - Single response for resources.
CallResourceis a single request and response round trip. The guest can’t stream multipleCallResourceResponsechunks. - Serialized calls. Grafana serializes every call into a single module instance, so concurrent queries for the same plugin queue rather than run in parallel.
- No hot reload. Changing the module requires restarting the plugin, which recompiles and re-instantiates it. The first start with a new module compiles without the cache (see “Module compilation and the cache”); later starts read from it.
Troubleshooting
If a WASM plugin doesn’t load or fails at runtime:
- Check the Grafana server log at debug level for
WASM plugin logandWASM plugin outputlines from the guest. - Confirm
plugin.jsondeclares"backendRuntime": "wasm"and thatwasmFilePathpoints at an existing module. - For blocked requests, look for the
Blocked outbound WASM plugin request to disallowed hostwarning and add the host toallowedOutboundHostsinplugin.jsonorallowed_outbound_hostsin[plugins.wasm_sandbox]. Grants from an unsigned plugin manifest are ignored, so add the host to the configuration in that case. - For
module closed with exit_code(0)errors, verify the module is built asc-sharedforwasip1and exports_initialize, not_start.