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_fetch and host_log host 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:

  1. Core plugins, such as the built-in data sources.
  2. The WASM provider, which claims the plugin only if it declares itself as WASM.
  3. The default gRPC provider, which handles everything else.

The WASM provider claims a plugin when either condition holds:

  • plugin.json sets backendRuntime to "wasm".
  • The plugin’s executable path ends in .wasm, which covers plugins that don’t set backendRuntime but 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.

JSON
{
  "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 to true so 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 regular executable path 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:

  1. The wasmSandbox block of plugin.json. Honoured only when the plugin signature is valid or internal. 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.
  2. [plugins.wasm_sandbox.<plugin id>]. Entries for one plugin, set by an administrator.
  3. [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].

ini
[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
Key Type Description
allowed_outbound_hosts list Hosts, as host or host:port, the guest may reach through host_http_fetch. Compared case-insensitively; a host without a port doesn’t match the same host with a port.
allowed_env_vars list Names of host environment variables forwarded to the guest module. Names missing from the Grafana server environment are skipped.
allowed_read_paths list Host directories mounted read-only into the guest filesystem, as hostPath:guestPath. A bare path is mounted at the same path in the guest.
ignore_manifest_grants boolean Ignore the wasmSandbox block of plugin.json for all plugins. Only valid in the parent section.

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) uint32 and free(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 a pluginv2.QueryDataRequest and return a pluginv2.QueryDataResponse.
  • wasm_check_health: Handle a pluginv2.CheckHealthRequest and return a pluginv2.CheckHealthResponse.
  • wasm_call_resource: Handle a pluginv2.CallResourceRequest and return a pluginv2.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 with method, url, optional headers, and optional body. The host validates the destination against the sandbox allow-list, performs the request with Grafana’s HTTP client, and writes back a JSON payload with status, headers, body, and an error field. Only http and https URLs 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.

sh
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 allowedOutboundHosts in plugin.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, and CallResource. Every other backend method, including metrics collection, chunked queries, streaming, admission, and conversion, returns plugins.ErrMethodNotImplemented.
  • Single response for resources. CallResource is a single request and response round trip. The guest can’t stream multiple CallResourceResponse chunks.
  • 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:

  1. Check the Grafana server log at debug level for WASM plugin log and WASM plugin output lines from the guest.
  2. Confirm plugin.json declares "backendRuntime": "wasm" and that wasmFilePath points at an existing module.
  3. For blocked requests, look for the Blocked outbound WASM plugin request to disallowed host warning and add the host to allowedOutboundHosts in plugin.json or allowed_outbound_hosts in [plugins.wasm_sandbox]. Grants from an unsigned plugin manifest are ignored, so add the host to the configuration in that case.
  4. For module closed with exit_code(0) errors, verify the module is built as c-shared for wasip1 and exports _initialize, not _start.