Cloud Spanner instances billing node-hours with zero requests
What does ZopNight detect here?
Cloud Spanner bills per node-hour whether or not queries arrive. ZopNight reads the SpannerRequestCount series for each instance and flags only those whose average and maximum both sit at 0 across the full window, treating the entire monthly bill as recoverable because an unqueried instance has no partial value.
Signal and threshold
| Field | Value |
|---|---|
| Rule IDs | RC-1243 |
| Category | idle |
| Severity | high |
| Metric | none — pure configuration read |
| Source | spanner_idle.go |
Where it applies
Node-hours accrue without queries
Spanner is provisioned capacity: you pay per node-hour (or per processing unit) from the moment the instance exists, and the database keeps its regional replicas running whether applications query it or not. A Spanner instance is one of the more expensive things to forget: even a single-node regional instance runs to hundreds of dollars a month. That is why this rule carries high severity and a delete lever rather than a resize.
One metric, read by name
A Spanner instance surfaces 6 different monitoring series, so the rule never reads
whichever metric happens to come first for the resource. It looks up SpannerRequestCount
by its exact name and fires only when both the average and the maximum of the full
harvested series are 0. Any measured request, however small, clears the instance. The read
path is Cloud Monitoring’s monitoring.timeSeries.list permission, part of
roles/monitoring.viewer.
The savings math is deliberately unclever: zero requests means 100% of the node-hours are waste, so the recoverable amount is the instance’s full monthly cost, and the optimized cost is $0.
Query the request count yourself
curl -s -H "Authorization: Bearer $(gcloud auth print-access-token)" \ "https://monitoring.googleapis.com/v3/projects/PROJECT_ID/timeSeries?filter=metric.type%3D%22spanner.googleapis.com%2Fapi%2Fapi_request_count%22&interval.startTime=$(date -u -v-30d +%Y-%m-%dT%H:%M:%SZ)&interval.endTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)"An empty response over 30 days corroborates what the rule saw.
Before the delete
Export the data first; the Dataflow template “Cloud Spanner to Avro Files on Cloud Storage” is the standard route. Then delete the instance to stop the per-node charge. If the workload is real but bursty, the Spanner autoscaler is the middle path between always-on nodes and deletion.
Why some idle instances never surface
The rule abstains whenever pricing for the instance is unknown or zero, and whenever the
SpannerRequestCount series is absent. There is intentionally no metadata fallback:
request activity is a monitoring time series, not an asset-inventory field, so an
uninstrumented instance produces no recommendation rather than a guessed one. A
high-severity delete is only ever backed by measured data.