When you attach a WebAssembly (Wasm) plugin to an Application Load Balancer with Service Extensions, the load balancer's extension chain (LbEdgeExtension, LbRouteExtension, or LbTrafficExtension) doesn't reference a specific plugin version. Instead, the extension chain points to the parent WasmPlugin resource, and the plugin's mainVersionId field controls which WasmPluginVersion actually executes on the proxy path.
That indirection makes rolling back a bad release easy, but it also makes it easy for a CI/CD job to stage a new build without ever serving it. If a pipeline pushes a new Wasm image to Artifact Registry and runs gcloud service-extensions wasm-plugin-versions create | Google Cloud SDK, Google Cloud creates the new version resource, yet mainVersionId stays pinned to the previous version until you update the parent plugin.
What the default CLI output leaves out
If you check your plugins with gcloud service-extensions wasm-plugins list | Google Cloud SDK, the default table shows the active MAINVERSION for each plugin, but it doesn't tell you if a newer version is sitting unpromoted:
~ gcloud service-extensions wasm-plugins list \
--project="my-gcp-project" \
--location="global"
NAME DESCRIPTION MAINVERSION UPDATETIME
auth-header-plugin Injects tenant auth headers v1 2026-10-14T16:20:11.402198Z
bot-score-plugin Tags requests with bot score header v3 2026-11-05T09:12:44.118004Z
To see whether auth-header-plugin has a newer version waiting, you have to query its version history separately with gcloud service-extensions wasm-plugin-versions list | Google Cloud SDK:
~ gcloud service-extensions wasm-plugin-versions list \
--project="my-gcp-project" \
--location="global" \
--wasm-plugin="auth-header-plugin"
NAME WASM_IMAGE WASM_IMAGE_DIGEST CONFIG_SIZE CONFIG_IMAGE CONFIG_IMAGE_DIGEST UPDATE_TIME
v1 us-south1-docker.pkg.dev/my-gcp-project/wasm-plugins/auth-header:1.0.0 sha256:4a8b91e0... - 2026-10-14
v2 us-south1-docker.pkg.dev/my-gcp-project/wasm-plugins/auth-header:1.1.0 sha256:7c3d2e1f... - 2026-11-08
Here, v2 was uploaded on 2026-11-08, while the plugin is still serving v1 from October. Even after running both commands, neither table shows which load balancer extensions are actually calling auth-header-plugin.
How the API links extension chains to plugin versions
If you pull the raw JSON for a plugin from Method: projects.locations.wasmPlugins.get | Service Extensions (or pass --format=json to gcloud), the response includes a usedBy array listing every load balancer extension attached to that plugin:
{
"description": "Injects tenant auth headers",
"logConfig": {
"enable": true,
"minLogLevel": "INFO",
"sampleRate": 1.0
},
"mainVersionId": "v1",
"name": "projects/my-gcp-project/locations/global/wasmPlugins/auth-header-plugin",
"updateTime": "2026-10-14T16:20:11.402198Z",
"usedBy": [
{
"name": "projects/my-gcp-project/locations/global/lbRouteExtensions/prod-alb-route-ext"
},
{
"name": "projects/my-gcp-project/locations/global/lbEdgeExtensions/prod-alb-edge-ext"
}
]
}
On the load balancer side, each extension chain binds to the parent wasmPlugins/auth-header-plugin path in its service field rather than a versioned URL, as covered in Create a plugin | Service Extensions - Google Cloud Documentation:
"extensionChains": [
{
"extensions": [
{
"failOpen": false,
"forwardHeaders": [
"x-tenant-id",
"authorization"
],
"name": "normalize-tenant-auth",
"service": "projects/my-gcp-project/locations/global/wasmPlugins/auth-header-plugin",
"supportedEvents": [
"REQUEST_HEADERS"
],
"timeout": "0.100s"
}
],
"matchCondition": {
"celExpression": "request.path.startsWith('/api/v1/')"
},
"name": "api-v1-chain"
}
]
Meanwhile, the individual WasmPluginVersion objects returned by Method: projects.locations.wasmPlugins.versions.list | Service Extensions record the createTime and the exact imageDigest resolved from Artifact Registry:
{
"createTime": "2026-11-08T21:04:18.892310Z",
"imageDigest": "sha256:7c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d",
"imageUri": "us-south1-docker.pkg.dev/my-gcp-project/wasm-plugins/auth-header:1.1.0",
"name": "projects/my-gcp-project/locations/global/wasmPlugins/auth-header-plugin/versions/v2",
"updateTime": "2026-11-08T21:04:18.892310Z"
}
Checking for unpromoted versions across a project
Instead of inspecting each plugin one at a time, we can join the wasm-plugins list and wasm-plugin-versions list JSON responses with jq to compare mainVersionId against the newest version by createTime and print the attached load balancer extensions:
#!/bin/bash
PROJECT="<YOUR_PROJECT_ID>"
LOCATION="global"
gcloud service-extensions wasm-plugins list \
--project="$PROJECT" \
--location="$LOCATION" \
--format=json | \
jq -c '.[]' | while read -r plugin; do
PLUGIN_NAME=$(echo "$plugin" | jq -r '.name | split("/") | last')
gcloud service-extensions wasm-plugin-versions list \
--project="$PROJECT" \
--location="$LOCATION" \
--wasm-plugin="$PLUGIN_NAME" \
--format=json | \
jq -r --argjson plugin "$plugin" '
$plugin as $p |
($p.name | split("/") | last) as $name |
$p.mainVersionId as $active_ver |
(sort_by(.createTime) | last) as $latest |
($latest.name | split("/") | last) as $latest_ver |
(map(select((.name | split("/") | last) == $active_ver))[0]) as $active |
([($p.usedBy // [])[] | .name | split("/") | last] | if length > 0 then join(", ") else "none" end) as $exts |
"Plugin: \($name) | Active: \($active_ver) (\($active.imageDigest[0:19])) | Latest: \($latest_ver) (\($latest.imageDigest[0:19])) | Drift: \($active_ver != $latest_ver) | Used By: \($exts)"
'
done
Here is what the output looks like against our project:
~ bash wasm-plugin-version-check.sh
Plugin: auth-header-plugin | Active: v1 (sha256:4a8b91e0c3f2) | Latest: v2 (sha256:7c3d2e1f0a9b) | Drift: true | Used By: prod-alb-route-ext, prod-alb-edge-ext
Plugin: bot-score-plugin | Active: v3 (sha256:8e9f0a1b2c3d) | Latest: v3 (sha256:8e9f0a1b2c3d) | Drift: false | Used By: prod-alb-traffic-ext
Promoting versions and a few sharp edges
When Drift: true shows up, you can promote the version or adjust your deployment workflow with a few rules in mind:
- Uploading vs. promoting in one command: Running
wasm-plugin-versions createonly stages the version. If you want your CI/CD job to create a new version and switchmainVersionIdin one step, use gcloud service-extensions wasm-plugins update | Google Cloud SDK and pass both--main-versionand--image. If the version already exists (likev2above), rungcloud service-extensions wasm-plugins update auth-header-plugin --main-version=v2 --location=globalto roll all attachedusedByextensions forward without touching your load balancer configs. - Mutable container tags don't auto-update: Service Extensions resolves the container tag to its immutable
sha256imageDigestwhen theWasmPluginVersionis created, as described in Manage plugins | Service Extensions - Google Cloud Documentation. Pushing a new image to the same:latesttag in Artifact Registry won't change what an existingWasmPluginVersionruns - you still need to create a new version and updatemainVersionId. - Cleaning up old versions: The API won't let you delete the
WasmPluginVersioncurrently referenced bymainVersionId. Promote the new version first before pruning older versions.
