Prometheus 3.11 → 3.12
caution
2 observations
prometheus suite: Downgrades and replays the write-ahead log with the st-storage flag on and off; with it on, data loss is the expected result.
- [prometheus] st-storage=ON 3.12->3.11 WAL replay: REPLAY_DATA_LOSS
- [prometheus] st-storage=OFF 3.12->3.11 WAL replay: REPLAY_CLEAN
One of 158 published runbooks, shown in full. Agents fetch the others with get_runbook.
Summary
Prometheus 3.12.0 is primarily a security, feature and performance release. The severity here is caution rather than safe because Remote Write now rejects snappy payloads declaring a decoded length over 32MB, gzip-encoded OTLP bodies are capped at a decompressed size, and one bugfix converts a previously-silent remote_write misconfiguration into a load-time error (v3.12.0 release notes).
Two CVE-class fixes make this a recommended upgrade for anyone running Prometheus as a Remote Write receiver or using STACKIT service discovery (v3.12.0 release notes).
Notable additions you may want to adopt afterwards: /api/v1/status/self_metrics, a UI for deleting series and cleaning tombstones, DigitalOcean Managed Databases and Outscale VM SD, AWS SD external_id and EC2 IPv6, and the experimental use-start-timestamps / st-synthesis feature flags (v3.12.0 release notes).
The matching Helm chart for appVersion v3.12.0 is prometheus-29.13.1 (Chart.yaml).
Breaking changes, explained
1. Remote Write rejects snappy payloads declaring >32MB decoded length (remote-write-snappy-32mb-limit)
Prometheus now rejects snappy-compressed requests received via Remote Write whose declared decoded length exceeds 32MB — a decompression-bomb DoS fix (v3.12.0 release notes, #18642). If this Prometheus acts as a Remote Write receiver, senders emitting very large batches may now have writes rejected; review sender batch sizing.
2. STACKIT SD secret exposure fix (stackit-sd-secret-redaction)
Secrets in stackit_sd_configs were exposed in plaintext via the config endpoint; this is fixed under GHSA-39j6-789q-qxvh (v3.12.0 release notes, #18649). The release note names it /-/config, but 3.12.0 has no such route: config is served at /api/v1/status/config and shown on the /config page (web.go, api.go at v3.12.0). If you use STACKIT SD, treat any credential that was in the config as leaked and rotate it.
3. OTLP gzip decompressed body size limit (otlp-gzip-decompressed-size-limit)
gzip-encoded OTLP write requests now have their decompressed body size limited (v3.12.0 release notes, #18408). The decoder reads at most 32 MiB of decompressed body (decodeReadLimit in codec.go at v3.12.0).
4. remote_write queue_config validated at load time (remote-write-queue-config-validation)
queue_config fields are now validated when the configuration is loaded, to prevent runtime panic and silent misconfiguration (v3.12.0 release notes, #18209). Validate with a 3.12 promtool check config before rolling, so any config that 3.11 accepted but 3.12 rejects is caught off-cluster.
5. Consul SD: health_filter split from filter (consul-sd-health-filter-split)
A new health_filter field was added for Health API filtering, "fixing breakage when using Catalog-only fields like ServiceTags in filter" (v3.12.0 release notes, #18479, #18499). #18499 passes health_filter only to the Health API and keeps filter Catalog-only, so move Health-API predicates into health_filter and keep Catalog predicates in filter. v3.11.2 already ships this (#18499), so from 3.11.2 or later nothing changes here.
6. WAL histogram samples carry a Start Timestamp field (wal-histogram-start-timestamp-field)
The TSDB/Agent WAL now adds a Start Timestamp field to all WAL histogram samples in memory when the st-storage flag is enabled (v3.12.0 release notes, #18221). With the flag on, 3.12.0 writes histogram samples as the new WAL record types HistogramSamplesV2 and FloatHistogramSamplesV2 (record.go at v3.12.0). 3.11.3's WAL replay has no case for them and ends in default: // Noop. (head_wal.go at v3.11.3), so after a rollback to 3.11 those records are skipped and their histogram samples are not replayed.
7. SD metrics deleted on scrape job removal (sd-metric-deletion-on-job-removal)
prometheus_sd_refresh* and prometheus_sd_discovered_targets for a scrape job are now deleted when that job is removed (v3.12.0 release notes, #17614). Previously stale series lingered.
8. Behavioural changes worth flagging (non-breaking but visible)
sort,sort_by_label,sort_by_label_descinside range (matrix) queries now emit a warning annotation — the release notes describe these functions as ones that "do not have effect in that context" (release notes, #18498). Tooling that treats warnings as errors will now flag these queries.- Metric name is now correctly dropped when
last_over_time/first_over_timewraps a subquery containing name-dropping functions likeabs()(release notes, #18409). Recording rules or dashboards that relied on the retained__name__will see it disappear. info()now handles negated__name__matchers correctly (release notes, #17932) — results may differ from 3.11.- Duration expressions that are NaN, infinite or out of range are now rejected instead of silently coerced (release notes, #18639).
- SD target updates propagate via a dynamic backoff interval instead of a static 5s throttle (release notes, #18187), so target churn timing changes slightly.
Migration steps
Pre-flight config validation. Run against your live config, using a 3.12
promtool(this is what catches the newqueue_configvalidation before it takes down a pod). In the Helm chart, take the deployed values (they include any--setor extra-foverrides), render, then assert the extraction is non-empty:helm repo update helm get values <release> -n <ns> -o yaml > values.yaml helm template <release> prometheus-community/prometheus -n <ns> --version 29.13.1 -f values.yaml \ --show-only templates/cm.yaml > cm.yaml kubectl annotate --local -f cm.yaml x=y -o jsonpath='{.data.prometheus\.yml}' > prometheus.yml test -s prometheus.yml || { echo "extraction failed"; exit 1; }Then invoke
promtoolfrom the release image by overriding the entrypoint;--userruns it as you rather than the image'snobody, so it can read your files:docker run --rm --user "$(id -u)" --entrypoint=promtool -v "$PWD:/cfg" \ quay.io/prometheus/prometheus:v3.12.0 check config --syntax-only /cfg/prometheus.yml--syntax-onlyskips therule_filesand*_filepaths, which do not exist inside the container, but still loads the config, wherequeue_configis validated (config.go at v3.12.0).Grep for the affected config surfaces.
grep -nE 'stackit_sd_configs|consul_sd_configs|queue_config|auto-reload-config' prometheus.yml values.yaml- Any
stackit_sd_configs: rotate every credential that appeared in the config, since it may have been served in plaintext via/api/v1/status/configon 3.11 (release notes, GHSA-39j6-789q-qxvh). Rotate after the upgrade lands so the new credential is never exposed by the old binary. - Any
consul_sd_configswithfilter:: move Health-API predicates intohealth_filter:, leaving Catalog-only fields such asServiceTagsinfilter(#18499). On 3.11.0 or 3.11.1, which rejecthealth_filter, stage this edit for the upgrade apply; 3.11.2 and later accept it now.
- Any
Leave the
auto-reload-configflag as it is. The feature is promoted to stable in 3.12 (release notes, #18620), but 3.12.0 still accepts--enable-feature=auto-reload-config, logging "This option for --enable-feature is deprecated. Use --config.auto-reload instead." (main.go at v3.12.0). Its replacement--config.auto-reloaddefaults tofalse, so deleting the old entry fromserver.extraFlags/server.extraArgswithout adding it turns auto-reload off. Switch only once you no longer need a 3.11 rollback: 3.11.3 defines no--config.auto-reloadflag (main.go at v3.11.3). Noteserver.extraFlagsdefaults to[web.enable-lifecycle]in chart 29.13.1 (values.yaml).Check Remote Write sender batch sizes if this Prometheus is a receiver, so that snappy-compressed requests do not declare a decoded length above 32MB (release notes, #18642). Non-Prometheus senders (Grafana Agent/Alloy, Mimir distributors, custom clients) need the same review.
Check OTLP collector batch sizes if you use the OTLP write endpoint (release notes, #18408).
Capture pre-upgrade baselines:
kubectl -n <ns> get deploy,sts -l app.kubernetes.io/instance=<release>,app.kubernetes.io/component=server kubectl -n <ns> port-forward deploy/<name> 9090:9090 & # sts/<name> when server.statefulSet.enabled until curl -sf localhost:9090/-/ready >/dev/null; do sleep 1; done curl -sfG localhost:9090/api/v1/query --data-urlencode 'query=count by (job) (up)' \ > baseline-up.json curl -sfG localhost:9090/api/v1/query \ --data-urlencode 'query=sum by (config) (prometheus_sd_discovered_targets)' \ > baseline-sd.json test -s baseline-up.json && test -s baseline-sd.json || echo "FAIL: baseline capture"Then take an EBS snapshot of the Prometheus PVC.
kubectl get pvcdoes not expose the EBS volume id — resolve the bound PV and readspec.csi.volumeHandle(spec.awsElasticBlockStore.volumeIDfor in-treegp2volumes), then wait for the snapshot to complete:PVC=$(kubectl -n <ns> get pvc -l app.kubernetes.io/instance=<release>,app.kubernetes.io/component=server -o jsonpath='{.items[0].metadata.name}') PV=$(kubectl -n <ns> get pvc "$PVC" -o jsonpath='{.spec.volumeName}') VOL=$(kubectl get pv "$PV" -o jsonpath='{.spec.csi.volumeHandle}{.spec.awsElasticBlockStore.volumeID}'); VOL=${VOL##*/} : "${VOL:?PV $PV has no EBS volume id}" SNAP=$(aws ec2 create-snapshot --volume-id "$VOL" --description "prom pre-3.12" \ --query SnapshotId --output text) aws ec2 wait snapshot-completed --snapshot-ids "$SNAP"Record
$PVCand$SNAP. Do not proceed to step 7 unlessaws ec2 waitexits 0.Upgrade — image bump and the staged config edits in one atomic apply.
- First, re-render
prometheus.ymlfrom the editedvalues.yaml(Consulhealth_filtermoves included) and re-run the 3.12promtool check config --syntax-onlyon it. Do not apply if it fails. For Helm, diff the release against the new chart and stop on changes outside the Prometheus server:helm get manifest <release> -n <ns> > old.yaml helm template <release> prometheus-community/prometheus -n <ns> --version 29.13.1 -f values.yaml --no-hooks > new.yaml diff old.yaml new.yaml - Helm:
helm upgrade <release> prometheus-community/prometheus -n <ns> --version 29.13.1 -f values.yamlwith the staged edits already invalues.yaml, so the new ConfigMap and the new image land together. - Plain manifests: apply the new ConfigMap and the image bump to
quay.io/prometheus/prometheus:v3.12.0in a singlekubectl apply. - The chart's
server.image.tagdefaults to empty, in which case theappVersionfield from Chart.yaml is used (values.yaml), which for 29.13.1 is v3.12.0 (Chart.yaml). Ifvalues.yamlsetsserver.image.tag, set it tov3.12.0or delete it; otherwise the upgrade keeps the pinned 3.11 image androllout statusstill passes. - The chart default deployment strategy is
RecreatewithreplicaCount: 1andserver.statefulSet.enabled: false(values.yaml), so expect a short scrape gap during the roll. - Gate before Verify:
kubectl -n <ns> rollout status deployment/<name> --timeout=10m(statefulset/<name>whenserver.statefulSet.enabled: true). If it does not reach Ready, readkubectl -n <ns> logs <pod> -c prometheus-server --previousand go to Rollback.
- First, re-render
Rotate STACKIT credentials if you use STACKIT SD and
/api/v1/status/configwas reachable by anyone untrusted (release notes, GHSA-39j6-789q-qxvh).Do not enable experimental start-timestamp flags in the same change.
use-start-timestampschangesrate()/irate()/increase()/resets()(#18344, #18627),st-synthesisis new (#18279), andst-storagechanges WAL contents (#18221) (release notes). Ship the version bump first, adopt flags separately.
Gotchas
use-start-timestampsis incompatible withanchoredandsmoothedextended range selectors — the release notes state it "doesn't work together" with them (release notes, #18344).- Enabling
st-storagechanges WAL contents, because histogram WAL records gain a start-timestamp field (release notes, #18221). - New tombstone/series-deletion UI. A web interface for deleting time series and cleaning tombstones is added (release notes); it drives the
delete_seriesandclean_tombstonesadmin APIs and requires--web.enable-admin-api(#18390). The chart'sserver.extraFlagsshipsweb.enable-admin-apicommented out (values.yaml); if you enable the admin API, put authentication in front of Prometheus — anyone reaching it can delete series. prometheus_tsdb_head_chunksmay shift after upgrade. The metric going negative after WAL replay is fixed (release notes, #18401), so dashboards showing negative values will "jump" to correct values.- Percentage-based retention bugfixes. Decimal precision is now preserved in percentage-based retention calculations, and percentage retention works with the new data path (release notes, #18374, #18628).
- Consul SD
filter/health_filter. The fix addshealth_filterfor Health API filtering and repairs Catalog-only fields infilter(release notes, #18479); verify target counts per Consul job explicitly after the upgrade. - AWS EC2 SD IPv6: EC2 SD can now discover IPv6 addresses, but private IPv4 remains the default when both are present (release notes, #16088) — so existing dual-stack targets should not move. Verify anyway if you relabel on
__meta_ec2_*. - Go 1.26 / native histograms: a
FloatHistogram.KahanAddregression on Go 1.26 was addressed (release notes, #18568), and a panic querying overlapping native-histogram chunks was fixed (#18692). Heavy native-histogram users should see this as a stability win, not a risk.
EKS-specific notes
- RBAC: the chart generates the ClusterRole for
kubernetes_sd_configswithrbac.create: trueby default (values.yaml). - AWS SD
external_idfor ECS/MSK/RDS/Elasticache is new in 3.12 (release notes, #17171, #18579). If you cross-account assume-role into other AWS accounts from EKS with an external-ID-constrained trust policy, you can now setexternal_iddirectly in the SD config instead of working around it via a sidecar/proxy. Check target cardinality after enabling. - IRSA / Pod Identity is unchanged; the chart's
serviceAccounts.server.annotations: {}is where theeks.amazonaws.com/role-arnannotation goes (values.yaml). No change needed for 3.12. - EBS-backed PVC: the chart defaults to
server.persistentVolume.enabled: true,size: 8Gi,accessModes: [ReadWriteOnce],mountPath: /data(values.yaml). With the defaultstrategy.type: Recreate, an EBSReadWriteOncevolume detaches and re-attaches on upgrade — expect a period of unavailability while the new pod binds the volume in the same AZ. server.hostNetworkis documented in the chart as required on managed clusters such as AWS EKS with custom CNIs like Calico (values.yaml); it defaults tofalse. 3.12 does not change this.- PDB / PVC retention features:
server.podDisruptionBudget.unhealthyPodEvictionPolicyandserver.statefulSet.pvcDeleteOnStsDelete/pvcDeleteOnStsScaleare documented in the chart as requiring Kubernetes 1.27.0+ (values.yaml); confirm your cluster's version before relying on them.podDisruptionBudget.enableddefaults tofalse.
Verify after upgrade
First resolve the real workload name and open a local port; every check below runs against that forwarded port from your own machine.
kubectl -n <ns> get deploy,sts -l app.kubernetes.io/instance=<release>,app.kubernetes.io/component=server
kubectl -n <ns> port-forward deploy/<name> 9090:9090 & # sts/<name> when server.statefulSet.enabled
Version and startup:
curl -sf localhost:9090/api/v1/status/buildinfoExpect
"version":"3.12.0". If you prefer to exec into the pod, resolve the container name first and use whatever HTTP client the image provides (wgetorcurl):POD=$(kubectl -n <ns> get pod -l app.kubernetes.io/instance=<release>,app.kubernetes.io/component=server -o jsonpath='{.items[0].metadata.name}') kubectl -n <ns> get pod "$POD" -o jsonpath='{.spec.containers[*].name}' kubectl -n <ns> exec "$POD" -c <container-name> -- \ wget -qO- localhost:9090/api/v1/status/buildinfoConfig actually loaded (not just pod Running):
curl -s localhost:9090/api/v1/status/config | head -c 400 curl -sG localhost:9090/api/v1/query \ --data-urlencode 'query=prometheus_config_last_reload_successful'Expect the reload metric to be
1.STACKIT redaction (only if you use it). Fail closed — a failed request or empty body must not read as "redacted":
BODY=$(curl -sf localhost:9090/api/v1/status/config) || { echo "FAIL: config endpoint unreachable"; exit 1; } [ -n "$BODY" ] || { echo "FAIL: empty config body"; exit 1; } printf '%s' "$BODY" | jq -r .data.yaml | grep -i -A10 stackitThe endpoint returns JSON with the whole config in the
data.yamlstring, so decode it before grepping. Assertservice_account_keyandprivate_keyrender as<secret>, and fail the step if any credential value appears verbatim: #18649 (GHSA-39j6-789q-qxvh) makes themconfig.Secret, which marshals as<secret>(config.go in prometheus/common v0.67.5).Target counts unchanged — compare against the baselines captured in Migration step 6, which were produced by:
curl -sG localhost:9090/api/v1/query --data-urlencode 'query=count by (job) (up)' > baseline-up.json curl -sG localhost:9090/api/v1/query \ --data-urlencode 'query=sum by (config) (prometheus_sd_discovered_targets)' > baseline-sd.jsonNow re-run both queries against the upgraded server and diff against
baseline-up.json/baseline-sd.json. If those files are absent (e.g. you were handed only this section), capture them from a still-running 3.11 instance, or compare in-place against a lookback query such as:curl -sG localhost:9090/api/v1/query --data-urlencode 'query=count by (job) (up offset 1h)'Pay special attention to Consul and EC2 jobs.
Remote Write health. The two queries below are sender-side (remote-write queue manager) metrics — they say nothing about this server's role as a receiver:
curl -sG localhost:9090/api/v1/query \ --data-urlencode 'query=rate(prometheus_remote_storage_samples_failed_total[5m])' curl -sG localhost:9090/api/v1/query \ --data-urlencode 'query=rate(prometheus_remote_storage_samples_dropped_total[5m])'For the receiver role — where the new 32MB snappy declared-decoded-length rejection (release notes, #18642) and the OTLP gzip decompressed-size limit (#18408) apply — inspect HTTP status codes per handler on this server and the server logs:
curl -sG localhost:9090/api/v1/query --data-urlencode \ 'query=sum by (code) (rate(prometheus_http_requests_total{handler=~"/api/v1/write|/api/v1/otlp/v1/metrics"}[5m]))' kubectl -n <ns> logs <pod> -c prometheus-server --since=15m | grep -iE 'decompress|too large|snappy|otlp'Also check failure/retry counters on each remote sender (Alloy/Agent, Mimir distributor, OTel collector). Confirm the metric names used here still exist in 3.12 before treating an all-zero result as proof of health — a query returning no series is not the same as a rate of zero.
OTLP ingest (if used): watch for failed gzip-encoded OTLP writes and rising drop counters on the sending collector (release notes, #18408).
TSDB health after WAL replay:
curl -sG localhost:9090/api/v1/query --data-urlencode 'query=prometheus_tsdb_head_chunks' curl -sG localhost:9090/api/v1/query --data-urlencode 'query=prometheus_tsdb_wal_corruptions_total' curl -sG localhost:9090/api/v1/query --data-urlencode 'query=prometheus_tsdb_head_series'head_chunksshould be non-negative (release notes, #18401).Rules and alerts still evaluating:
curl -sG localhost:9090/api/v1/query \ --data-urlencode 'query=prometheus_rule_evaluation_failures_total' curl -sG localhost:9090/api/v1/query --data-urlencode 'query=count(ALERTS)'Watch for rules using
last_over_time/first_over_timeover subqueries withabs()-style functions producing differently-named output series (release notes, #18409).New endpoint smoke test:
curl -s localhost:9090/api/v1/status/self_metrics | head -c 200should return JSON (release notes, #18411).
Rollback
First re-resolve the objects this section acts on — do not rely on values from earlier sections:
kubectl -n <ns> get deploy,sts -l app.kubernetes.io/instance=<release>,app.kubernetes.io/component=server
helm get values <release> -n <ns> # current (3.12) values, incl. staged edits
helm history <release> -n <ns> # previous revision number
PVC=$(kubectl -n <ns> get pvc -l app.kubernetes.io/instance=<release>,app.kubernetes.io/component=server -o jsonpath='{.items[0].metadata.name}')
PV=$(kubectl -n <ns> get pvc "$PVC" -o jsonpath='{.spec.volumeName}')
VOL=$(kubectl get pv "$PV" -o jsonpath='{.spec.csi.volumeHandle}{.spec.awsElasticBlockStore.volumeID}'); VOL=${VOL##*/}
aws ec2 describe-snapshots --filters "Name=volume-id,Values=$VOL" \
--query 'Snapshots[].{Id:SnapshotId,State:State,Time:StartTime}'
The Consul health_filter: edit reverts only for a rollback to 3.11.0 or 3.11.1. health_filter shipped in v3.11.2 (#18499), so 3.11.2 and later accept it; 3.11.0 and 3.11.1 lack the field and load config with yaml.UnmarshalStrict (config.go at v3.11.0), so for them move those predicates back into filter:.
If you did not enable st-storage, use-start-timestamps or st-synthesis, rollback is straightforward:
- Helm:
helm rollback <release> <previous-revision> -n <ns>— or re-runhelm upgradepinningserver.image.tag: v3.11.xand the previous chart version. This reverts image and config together. - Plain manifests: revert the ConfigMap before, or in the same
kubectl applyas, the image revert.kubectl -n <ns> rollout undo deployment/<name>(orrollout undo statefulset/<name>whenserver.statefulSet.enabled: true) reverts only the pod template — not the ConfigMap — so if you use it, apply the reverted ConfigMap first and then verify the 3.11 pod starts and reloads its config successfully (prometheus_config_last_reload_successful == 1, pod not in CrashLoopBackOff). The chart defaultrevisionHistoryLimit: 10retains prior revisions (values.yaml). - Apply the
health_filter:→filter:revert named above, if it applies, as part of that same rollback apply.
If you enabled st-storage: 3.11 skips the histogram records 3.12 wrote with the flag on, and scaling down or snapshotting first does not change that. Roll back only if losing those histogram samples is acceptable; otherwise stay on 3.12. To return to the pre-upgrade data instead, create a volume from the pre-upgrade snapshot in the old volume's zone, bind it with a PV and PVC per the EBS CSI driver's static-provisioning example, and set server.persistentVolume.existingClaim to that PVC. Everything written since the snapshot is lost.
Rollback is not a remediation for the two security fixes. 3.12 fixes the Remote Write decompression DoS (#18642) and the STACKIT config-endpoint plaintext secret exposure (GHSA-39j6-789q-qxvh) (release notes); the cited sources do not describe rollback behaviour, but a 3.11 binary does not contain those fixes. If you must roll back, restrict network access to /api/v1/write, /api/v1/status/config and /config until you can move forward again.