Skip to content

Split View: 네이티브 히스토그램은 stable이 됐는데, 왜 아직 못 켜고 있나

|

네이티브 히스토그램은 stable이 됐는데, 왜 아직 못 켜고 있나

들어가며 — "stable"이라는 단어가 실제로 바꾼 것

Prometheus 네이티브 히스토그램은 오래 기다린 기능입니다. 공식 명세의 첫 문장이 이력을 요약합니다 — 2022년 11월 실험 기능으로 도입됐고, 이를 지원한 첫 서버 버전은 v2.40.0이며, --enable-feature=native-histograms 플래그로 켜야 했습니다. 그리고 같은 문단에 이런 문장이 있습니다.

Starting with v3.8.0, native histograms are supported as a stable feature.

3.8.0은 2025년 12월 2일에 릴리스됐습니다(GitHub 릴리스 기준, CHANGELOG 헤더 표기는 11월 28일). 3년 넘게 실험 딱지를 달고 있던 기능이 드디어 stable이 된 겁니다.

그런데 이 글을 쓰는 시점에서 더 중요한 사건은 따로 있습니다. 3.13이 2026년 7월 1일에 나왔고, 이게 새 LTS입니다. 그리고 지금 많은 조직이 돌리고 있을 3.5 LTS는 2026년 7월 31일에 지원이 끝납니다. 2주 뒤입니다. 릴리스 사이클 문서의 LTS 표를 그대로 옮기면 이렇습니다.

Release          Date          End of support   Status
Prometheus 2.53  2024-06-16    2025-07-31       End of life
Prometheus 3.5   2025-07-14    2026-07-31       Supported
Prometheus 3.13  2026-07-01    2027-07-31       Supported
TBD              2027-06       2028-07-31       Upcoming

두 사실을 겹치면 결론이 하나 나옵니다. 3.5 LTS(2025년 7월)는 네이티브 히스토그램이 stable이 되기 넉 달 전에 잘린 버전입니다. 그리고 LTS 문서에는 지원 범위에서 빠지는 항목이 명시돼 있습니다 — API 안정성 보장에서 unstable로 분류된 것들, 실험적 기능, 그리고 OpenBSD 지원. 즉 3.5 LTS를 타고 있었다면 네이티브 히스토그램은 애초에 LTS 지원 범위 밖의 실험 기능이었고, 3.13은 이 기능이 stable 상태로 들어간 첫 LTS입니다. LTS 트랙을 타는 조직에게는 지금이 "언젠가 보자"가 아니라 실제 결정 시점인 이유입니다.

클래식 히스토그램이 만드는 시계열의 개수

왜 이 기능을 원했는지부터 짚고 가겠습니다. 문제는 카디널리티입니다.

클래식 히스토그램은 노출 포맷에는 히스토그램처럼 생겼지만, 수집되는 순간 float 시계열 여러 개로 쪼개집니다. 명세는 이 분해를 이렇게 설명합니다 — summary와 (클래식 버전의) histogram은 수집 시 float 구성요소로 분해되고, 두 타입 모두 sum과 count 구성요소를 가지며, histogram은 여기에 다수의 bucket 샘플이 붙습니다.

실제로 어떻게 되는지는 명세가 든 예가 정확합니다. rpc_latency_seconds라는 히스토그램 하나는 클래식으로 수집하면 rpc_latency_seconds_sum, rpc_latency_seconds_count, 그리고 서로 다른 le 라벨을 가진 다수의 rpc_latency_seconds_bucket 시계열이 됩니다. 버킷이 N개면 시계열은 N+2개입니다. 네이티브 히스토그램으로 수집하면 이름 그대로 rpc_latency_seconds 시계열 하나입니다.

라벨로 파티셔닝하면 이 차이가 곱해집니다. 명세의 설명을 빌리면, 이미 무거운 클래식 히스토그램에 라벨을 붙여 100개의 히스토그램이 생기면 비용은 100배로 갑니다. 네이티브 히스토그램은 사정이 다릅니다 — 파티셔닝하면 개별 히스토그램들이 각자 더 적은 버킷만 채우는 경향이 있어서(상태 코드 404를 처리한 요청만 담는 히스토그램은 대개 좁은 분포를 가집니다), 전체 채워진 버킷 수는 히스토그램 개수 배수보다 훨씬 작게 늘어납니다. 명세가 "라벨로 파티셔닝하는 것조차 이제 훨씬 감당할 만하다"고 쓴 근거가 이것입니다.

여기서 숫자를 하나 지어내고 싶은 유혹이 있는데, 그러지 않겠습니다. Prometheus 명세와 Grafana Mimir 문서는 네이티브 히스토그램이 "클래식 히스토그램 대비 훨씬 낮은 리소스 비용으로 높은 해상도를 제공한다"고 서술하지만, 이는 벤더/프로젝트 문서의 정성적 서술이지 조건이 명시된 벤치마크 수치가 아닙니다. 절감 폭은 원래 버킷을 몇 개나 쓰고 있었는지, 분포가 얼마나 넓은지에 전적으로 좌우됩니다 — 버킷 5개짜리 히스토그램이라면 얻을 게 별로 없습니다.

스키마 — 버킷 경계가 설정이 아니라 공식인 이유

네이티브 히스토그램의 핵심 발상은 버킷 경계를 사람이 정하지 않고 공식으로 정하는 것입니다. 이걸 정하는 값이 스키마이고, 명세에 따르면 int8 정수로 현재 유효한 값은 -53, 그리고 -4부터 +8까지입니다. -53은 커스텀 버킷 경계(NHCB)용이고, 나머지가 표준 지수 스키마입니다.

표준 스키마 n에서 인덱스 i인 양수 버킷의 경계는 이렇게 계산됩니다(명세는 파이썬 문법으로 표기합니다).

# 상한(포함):  (2**2**-n)**i
# 하한(제외):  (2**2**-n)**(i-1)

여기서 중요한 성질이 나옵니다. 스키마 n은 스키마 n+1의 절반 해상도이고, 따라서 n+1짜리 히스토그램은 이웃 버킷을 병합해 n으로 변환할 수 있습니다. 이게 표준 스키마끼리 서로 병합 가능한 이유이고, 여러 인스턴스의 히스토그램을 합산하는 쿼리가 성립하는 근거입니다. 클래식 히스토그램에서 서로 다른 버킷 경계를 쓰는 두 서비스를 합칠 수 없던 문제가 여기서 사라집니다.

계기(instrumentation) 쪽에서는 스키마를 직접 고르지 않고 성장 인자로 지정합니다. Go 클라이언트의 NativeHistogramBucketFactor가 그것인데, client_golang 소스의 주석이 정확한 값을 알려 줍니다 — 비용과 정확도의 일반적으로 좋은 절충은 1.1이고(각 버킷이 이전 버킷보다 최대 10% 넓음), 이 값은 2의 거듭제곱 구간마다 8개 버킷으로 나뉘는 결과가 됩니다(1과 2 사이에 8개, 2와 4 사이에 8개, 4와 8 사이에 8개…). 실제 인자는 2^(2^-n) 으로 계산되며 n은 -4에서 8 사이 정수 중 지정값 이하로 가장 큰 인자를 주는 값이 선택됩니다. 즉 가능한 최소 인자는 약 1.00271(= 2^(2^-8))입니다.

0 근처는 따로 처리합니다. 지수 버킷은 0에 무한히 가까워지므로, 절댓값이 임계값 이하인 관측은 전부 "제로 버킷"에 모읍니다. Go 클라이언트의 기본 임계값은 DefNativeHistogramZeroThreshold = 2.938735877055719e-39입니다.

그리고 저장은 희소(sparse) 표현입니다 — 비어 있는 버킷은 (거의) 공짜입니다. 이게 "전체 float64 범위를 커버한다"는 말이 파산하지 않는 이유입니다. 커버하는 범위가 넓어도 실제로 채워진 버킷만 비용을 냅니다.

stable이 보장하지 않는 것들

여기서부터가 이 글의 본론입니다. stable 선언은 서버가 이 데이터 타입을 다루는 방식이 v4 전까지 깨지지 않는다는 뜻이지, "이제 그냥 켜면 된다"는 뜻이 아닙니다. 명세 자신이 stable 선언 바로 뒤에 단서를 답니다.

여전히 명시적으로 켜야 합니다. 스크레이프는 scrape_native_histograms 설정으로 활성화해야 하고, Remote-Write 전송은 send_native_histograms로 따로 켜야 합니다. 3.8에서는 기존 피처 플래그가 이 설정을 true로 만드는 효과만 남아 있었고, 3.9부터 플래그는 완전한 no-op이라 설정을 명시해야 합니다. 명세의 괄호가 로드맵을 알려 줍니다 — v4부터는 두 설정 모두 기본값이 true가 됩니다. 그러니까 지금은 "stable이지만 opt-in", v4는 "기본값"인 중간 구간입니다.

노출은 프로토버프로만 됩니다. 이게 실무에서 가장 자주 발목을 잡습니다. 명세는 클래식 Prometheus 텍스트 포맷은 네이티브 히스토그램용으로 확장되지 않았고 그럴 계획도 없다고 못 박습니다. OpenMetrics 텍스트 포맷 지원은 제안 문서가 있지만 구현 상태는 그대로 Not implemented입니다. OpenMetrics 프로토버프 쪽 지원 PR(OpenMetrics#256)은 2026년 3월 19일에 머지되지 않고 닫혔는데, 이유가 흥미롭습니다 — 사실상 브랜딩 정리입니다. Prometheus 프로토버프 노출 포맷이 이미 네이티브 히스토그램과 나머지 기능을 전부 지원하므로 그쪽을 정식 프로토버프 포맷으로 삼고, OpenMetrics는 텍스트 기반 포맷을 뜻하는 것으로 가자는 결정이었습니다. 정리하면 2026년 7월 현재, 네이티브 히스토그램을 노출하는 길은 클래식 Prometheus 프로토버프 포맷 하나뿐입니다.

클라이언트 라이브러리는 아직 따라오지 않았습니다. 이 부분은 직접 확인해 볼 가치가 있습니다. client_golang 최신 릴리스는 v1.23.2(2025년 9월 5일)인데, HistogramOpts의 주석에는 지금도 이 문장이 그대로 있습니다.

NOTE: Native Histograms are still an experimental feature. Their behavior might still change without a major version bump. Subsequently, all NativeHistogram... options here might still change their behavior or name (or might completely disappear) without a major version bump.

v1.23.2는 3.8.0(2025년 12월)보다 먼저 나온 릴리스이니 문구가 낡은 것이라고 볼 수 있고, 실제로 main 브랜치에도 아직 같은 문장이 남아 있습니다. 요점은 stable 선언의 주어가 서버라는 것입니다. 서버의 데이터 모델과 저장·쿼리 동작은 stable이지만, 계기 라이브러리의 옵션 API가 같은 보장을 문서상으로 주고 있지는 않습니다.

Python은 더 단순합니다 — client_python의 네이티브 히스토그램 구현 이슈는 아직 열려 있습니다. 텍스트 포맷 제안서가 밝힌 동기 중 하나가 바로 이것입니다. Python 클라이언트 같은 일부 라이브러리는 네이티브 히스토그램을 노출하자고 프로토버프 의존성을 추가하고 싶어 하지 않는다는 것.

쿠버네티스는 알파입니다. 쿠버네티스 컴포넌트 메트릭에 네이티브 히스토그램이 들어온 건 v1.36(2026년 4월 22일 릴리스)부터인데, NativeHistograms 피처 게이트의 상태는 알파, 기본값 false입니다. 적용 대상은 kube-apiserver, kube-controller-manager, kube-scheduler, kubelet, kube-proxy이고 컴포넌트별로 따로 켤 수 있습니다. 켜면 클래식과 네이티브를 동시에 노출하는 이중 노출(dual exposition) 방식이고, 어느 쪽을 받을지는 요청의 Accept 헤더가 결정합니다.

프로토버프 스크레이프 비용 — 원래 stable의 발목을 잡던 문제

여기 흥미로운 이력이 하나 있습니다. scrape_native_histograms를 켜면 콘텐츠 협상이 바뀌어 해당 스크레이프 설정이 프로토버프를 선호하게 됩니다. 명세가 밝히듯 기본값이 지정되지 않은 경우 유효 우선순위는 이렇게 바뀝니다.

[ PrometheusProto, OpenMetricsText1.0.0, OpenMetricsText0.0.1, PrometheusText0.0.4 ]

문제는 이게 그 스크레이프 설정의 모든 타깃에 적용된다는 점이었습니다. 네이티브 히스토그램 몇 개 받자고 전체 스크레이프를 프로토버프로 갈아타는 셈입니다. 이슈 #14668이 이 문제를 정면으로 다뤘고, 본문의 진단은 이랬습니다 — 프로토버프 파싱이 메모리 처닝을 많이 만들고, 대부분 GC CPU 사용량 증가로 나타나며, 사용 패턴에 따라 CPU가 크게 늘 수 있고 실제 사례에서 2배 증가를 본 적이 있다는 것. 그리고 같은 본문에 이런 문장이 굵게 적혀 있습니다.

(Or in other words: This issue needs to get resolved to declare native histograms a stable feature.)

즉 이 성능 문제는 stable 선언의 전제 조건이었습니다. 이슈는 2025년 2월 13일 completed로 닫혔고, 그 뒤 2025년 12월에 stable 선언이 나왔습니다. 닫히기 전 논의에서 메인테이너는 최적화 이후 현실적인 벤치마크에서 텍스트 파서 대비 3배 빠르고 할당 메모리는 10% 적다는 결과를 보고했습니다.

여기서 정직해야 할 부분이 있습니다. 명세 문서에는 아직도 텍스트 기반 포맷으로 스크레이프하는 것이 프로토버프보다 일반적으로 리소스를 훨씬 덜 쓴다는 note가 남아 있고, 근거로 바로 이 이슈를 링크합니다 — 그런데 그 이슈는 completed로 닫혀 있습니다. 문서의 note가 최적화 이전 상태를 반영한 낡은 서술로 보입니다. 그러니 "프로토버프 스크레이프는 여전히 2배 비싸다"고 말하는 것도, "이제 완전히 해결됐다"고 말하는 것도 지금 자료로는 과합니다. 당신 환경에서 CPU를 직접 재 보는 수밖에 없습니다. 켜기 전후로 스크레이프 대상 프로세스와 Prometheus 자신의 CPU를 비교하십시오.

한편 이 이슈가 남긴 실용적 유산이 있습니다. 노출 범위를 좁히는 수단들입니다. scrape_protocols를 직접 지정하면 scrape_native_histograms가 켜져 있어도 특정 타깃은 비프로토버프 포맷으로 강제할 수 있습니다. 그리고 3.13에는 릴레이블링으로 타깃별 덮어쓰기를 하는 내부 라벨이 추가됐습니다 — __scrape_native_histograms____always_scrape_classic_histograms__(#18929), 그리고 __convert_classic_histograms_to_nhcb__(#18840)입니다. 스크레이프 설정을 쪼개지 않고도 네이티브 히스토그램을 노출하는 타깃에만 정확히 켤 수 있다는 뜻입니다.

NHCB — 계기 코드를 건드리지 않는 경로

모든 클래식 히스토그램을 네이티브로 다시 계기하는 건 큰 작업입니다. 그래서 중간 경로가 있습니다. 스키마 -53, 즉 커스텀 버킷을 가진 네이티브 히스토그램(NHCB)입니다.

convert_classic_histograms_to_nhcb 스크레이프 옵션을 켜면 Prometheus가 수집 시점에 클래식 히스토그램을 NHCB로 변환해 넣습니다. 계기 코드는 그대로 두고 저장 효율만 가져오는 방식입니다. 명세의 표현으로 NHCB의 핵심 장점은 저장 비용이 일반적으로 훨씬 싸다는 것이고, 특히 버킷을 추가하는 한계 비용이 낮아서 버킷이 많은 클래식 히스토그램도 감당할 만하게 수집할 수 있습니다.

대신 대가가 명확합니다. NHCB는 서로 병합이 근본적으로 제한됩니다. 명세는 이렇게 경고합니다 — 서로 다른 버킷 레이아웃 간 자동 조정은 경계가 정확히 일치하는 것만 남기므로, 대부분의 경계가 일치하면 쓸 만한 결과가 나오지만, 버킷 레이아웃이 임의로 달라지면 어느 경계도 일치하지 않아 오버플로 버킷 하나만 남는 히스토그램이 나올 수 있습니다. 그래서 명세는 스키마 -53을 특정 용례에서 정보에 근거한 결정으로만 쓰라고 권합니다. 서비스마다 버킷 경계가 제각각인 코드베이스에서 NHCB로 전부 밀어 넣고 합산 쿼리를 돌리면, 조용히 쓸모없는 결과를 받게 됩니다.

정직한 트레이드오프 — 마이그레이션이 어려운 이유

명세의 마이그레이션 섹션은 문제를 세 가지로 정리합니다. 이 부분이 이 기능 도입의 실제 난이도입니다.

첫째, 쿼리 방식이 다릅니다. 대부분의 경우 변경은 최소한이고 직관적이지만, 까다로운 엣지 케이스가 있어서 신뢰할 만한 자동 변환이 어렵다는 게 명세의 서술입니다. 즉 대시보드와 알림 규칙을 손으로 옮겨야 합니다.

둘째, 클래식과 네이티브는 서로 집계되지 않습니다. 이게 가장 아픕니다. 어느 시점에 클래식에서 네이티브로 갈아타면 그 전환 시점을 걸치는 대시보드를 만들기 어렵고, 전환 시점을 포함하는 레인지 벡터는 필연적으로 불완전합니다 — 클래식을 고르면 앞부분 데이터만, 네이티브를 고르면 뒷부분 데이터만 들어옵니다. 명세가 든 예가 구체적입니다.

histogram_quantile(0.9, rate(rpc_duration_seconds[1d]))

하루치 90분위 지연을 계산하는 이 쿼리는, 네이티브 히스토그램을 최소 하루 이상 모으지 않았다면 그보다 짧은 구간만 커버합니다. 그래서 명세의 권고는 보수적입니다 — 클래식과 네이티브를 한동안 병행 수집하라는 것. 한 달을 병행했다면 한 달보다 더 뒤를 보지 않는 대시보드는 전환 시점을 고민할 필요 없이 그냥 갈아탈 수 있습니다. 병행 수집은 always_scrape_classic_histograms 옵션으로 켭니다. 클래식은 접미사가 붙은 시계열들로, 네이티브는 이름 그대로의 시계열 하나로 들어가므로 TSDB에서 충돌하지 않습니다. 물론 그동안은 양쪽 비용을 다 냅니다.

셋째, 임의 버킷 경계를 못 잡습니다. 이게 SLO를 하는 팀에게 결정적일 수 있습니다. 클래식 히스토그램은 관심 지점에 정확히 경계를 두도록 맞춤 설계할 수 있습니다 — 예컨대 "300ms 이내 응답 비율"이 SLO라면 경계를 300ms에 박아 두면 그 비율은 추정이 아니라 정확한 값입니다. 표준 스키마의 네이티브 히스토그램은 해상도는 높지만 경계를 임의 값에 둘 수 없습니다. Grafana Mimir 문서도 같은 지적을 단점으로 명시합니다 — SLO 정의에 특히 중요한 임계값 같은 임의 버킷 경계를 설정할 방법이 없고, 주어진 임계값 위아래 관측 비율은 일반적으로 보간(interpolation)으로 추정해야 한다는 것. 명세는 이런 경우 "네이티브 히스토그램에서의 사용자 경험이 실제로 더 나빠질 수 있다"고 솔직하게 적어 뒀고, 해법으로 그 히스토그램은 그냥 마이그레이션하지 않고 두는 선택지를 제시합니다.

여기에 하나 더. 버킷은 처음 채워질 때 동적으로 생성되므로, 예상보다 넓은 분포가 들어오면 예상보다 많은 버킷이 생기고 메모리를 더 씁니다. 명세는 관측값이 외부에서 조작 가능하다면 이게 메모리 고갈을 통한 DoS 벡터가 될 수 있다고까지 경고합니다. 그런데 Go 클라이언트의 NativeHistogramMaxBucketNumber 기본값은 0, 즉 무제한입니다(명세도 이 점을 TODO로 남겨 두고 있습니다). 흔히 쓰는 값은 160이고, 이는 OTel 지수 히스토그램의 기본값과 같습니다. 수집 측에서도 방어할 수 있습니다 — 스크레이프 설정의 native_histogram_bucket_limit(개별 히스토그램의 버킷 수 상한)과 native_histogram_min_bucket_factor(성장 인자 하한)가 그것이고, 둘 다 초과 시 해상도를 낮춰 맞춥니다. 다만 NHCB가 버킷 수 상한을 넘기면 해상도를 낮출 수 없으므로 스크레이프가 실패합니다.

그래서, 당신은 켜야 하나

지금 켤 만한 경우

  • 버킷이 많은(수십 개) 클래식 히스토그램을 쓰고 있고, 라벨 파티셔닝 때문에 카디널리티가 이미 아픈 곳. 절감 폭이 원래 버킷 수에 비례하므로 여기가 가장 값을 합니다.
  • 지연 분포처럼 범위를 미리 알기 어려운 관측값. 버킷 경계를 추측해 박아 두는 일 자체가 사라지는 게 실질적 이득입니다.
  • Go로 계기하고 있고 서버를 3.13 LTS로 올릴 계획인 곳. 지금 스택에서 경로가 가장 매끄럽습니다.
  • 계기 코드를 못 건드리지만 저장 비용을 줄이고 싶은 곳 — 이 경우 NHCB 변환만 켜는 선택지가 있습니다. 단 병합 제약을 먼저 이해하고 쓰십시오.

아직 아닌 경우

  • SLO 임계값에 정확히 맞춘 버킷 경계에 의존하는 히스토그램. 보간 추정으로 바꾸는 순간 SLO 계산의 성격이 달라집니다. 이건 그냥 클래식으로 두십시오.
  • 계기가 Python client인 곳. 아직 미구현이라 선택지가 없습니다.
  • 버킷이 몇 개 안 되는 히스토그램. 얻을 게 별로 없는데 마이그레이션 비용은 그대로 냅니다.
  • 스크레이프 대상 대부분이 네이티브 히스토그램을 노출하지 않는데 스크레이프 설정 단위로 전부 프로토버프로 넘어가는 구성. 3.13의 타깃별 릴레이블링으로 범위를 좁히거나, scrape_protocols로 통제하십시오.
  • 쿠버네티스 컴포넌트 메트릭이 목적인 곳. 1.36 알파에 기본 비활성이므로, 프로덕션 클러스터에서 알파 피처 게이트를 켜는 결정을 따로 해야 합니다.

마치며

정리하면 이렇습니다. 네이티브 히스토그램은 실제로 stable이 됐고(3.8.0, 2025년 12월), 3.13이 이 기능을 stable로 담은 첫 LTS이며(2026년 7월 1일), 3.5 LTS는 7월 31일에 끝납니다. 데이터 모델은 진짜 좋습니다 — 시계열 N+2개가 1개가 되고, 버킷 경계를 추측하지 않아도 되고, 표준 스키마끼리 병합됩니다.

그런데 stable이라는 단어의 주어를 정확히 읽어야 합니다. 그건 서버가 이 타입을 다루는 방식이 v4 전까지 안 깨진다는 뜻이지, 생태계가 준비됐다는 뜻이 아닙니다. 노출은 아직 프로토버프 하나뿐이고, Go 클라이언트는 자기 옵션을 여전히 실험적이라고 문서화하고 있고, Python은 미구현이고, 쿠버네티스는 알파입니다. v4에서 기본값이 켜지는 방향은 분명하지만, 그 사이 구간을 지금 건너는 사람은 각자 이 구멍들을 확인해야 합니다.

그러니 실무적인 순서는 이렇습니다. 3.13으로 올리는 건 어차피 7월 31일 때문에 해야 하는 일이니 그것부터 하고, 네이티브 히스토그램은 별개의 결정으로 두십시오. 그다음 카디널리티가 가장 아픈 히스토그램 몇 개만 골라 병행 수집으로 시작하고, 프로토버프 전환의 CPU 영향을 직접 재고, 대시보드를 옮길 시간을 벌 만큼 병행 기간을 길게 잡으십시오. 3년 걸려 stable이 된 기능입니다. 2주 만에 다 옮길 이유는 없습니다.

참고 자료

Native Histograms Are Stable Now — So Why Haven't You Turned Them On Yet

Introduction — What the Word "Stable" Actually Changed

Prometheus native histograms have been a long time coming. The first sentence of the official spec summarizes the history — introduced as an experimental feature in November 2022, first supported in server version v2.40.0, and requiring the --enable-feature=native-histograms flag to turn on. And in the same paragraph:

Starting with v3.8.0, native histograms are supported as a stable feature.

3.8.0 was released on December 2, 2025 (per the GitHub release; the CHANGELOG header says November 28). A feature that had worn the "experimental" tag for more than three years was finally stable.

But at the time of writing, there's a more urgent event. 3.13 came out on July 1, 2026, and it's the new LTS. And the 3.5 LTS that many organizations are likely running right now goes end-of-support on July 31, 2026. That's two weeks out. Here's the LTS table from the release cycle doc, reproduced as-is.

Release          Date          End of support   Status
Prometheus 2.53  2024-06-16    2025-07-31       End of life
Prometheus 3.5   2025-07-14    2026-07-31       Supported
Prometheus 3.13  2026-07-01    2027-07-31       Supported
TBD              2027-06       2028-07-31       Upcoming

Overlay the two facts and one conclusion falls out. The 3.5 LTS (July 2025) was cut four months before native histograms went stable. And the LTS doc explicitly names what's excluded from support scope — things classified unstable under the API stability guarantee, experimental features, and OpenBSD support. In other words, if you were on 3.5 LTS, native histograms were an experimental feature outside the LTS support scope from the start, and 3.13 is the first LTS to carry this feature in stable state. That's why, for organizations on the LTS track, this is an actual decision point right now — not "we'll get to it eventually."

The Timeseries Count a Classic Histogram Produces

Let's start with why this feature was wanted in the first place. The problem is cardinality.

A classic histogram looks like a histogram in the exposition format, but the moment it's scraped it splits into multiple float timeseries. The spec describes this decomposition as follows — summaries and (the classic version of) histograms are decomposed into float components upon ingestion, and both types have sum and count components, with histograms additionally carrying a number of bucket samples.

The spec's own example shows exactly what happens in practice. A single histogram called rpc_latency_seconds, scraped classically, becomes rpc_latency_seconds_sum, rpc_latency_seconds_count, and a number of rpc_latency_seconds_bucket timeseries with different le labels. If there are N buckets, that's N+2 timeseries. Scraped as a native histogram, it's one rpc_latency_seconds timeseries, exactly as the name suggests.

This difference multiplies when you partition by labels. In the spec's own words, if you attach labels to an already-heavy classic histogram and end up with 100 histograms, the cost goes up 100x. Native histograms are different — when partitioned, the individual histograms each tend to fill fewer buckets (a histogram that only holds requests with status code 404 usually has a narrow distribution), so the total number of filled buckets grows much more slowly than a multiple of the histogram count. That's the basis for the spec's claim that "even partitioning by labels becomes much more affordable now."

There's a temptation here to invent a number, and I won't. Both the Prometheus spec and the Grafana Mimir docs describe native histograms as offering "higher resolution at much lower resource cost than classic histograms" — but that's a qualitative statement from vendor/project docs, not a conditioned benchmark figure. The magnitude of the savings depends entirely on how many buckets you were already using and how wide your distribution is — a histogram with 5 buckets won't gain much.

The Schema — Why Bucket Boundaries Are a Formula, Not a Setting

The core idea of native histograms is that bucket boundaries are determined by a formula rather than set by a human. The value that determines this is the schema, and per the spec it's an int8 integer whose currently valid values are -53, and -4 through +8. -53 is for custom bucket boundaries (NHCB); the rest are the standard exponential schemas.

For standard schema n, the boundaries for the positive bucket at index i are computed as follows (the spec writes it in Python syntax).

# upper bound (inclusive):  (2**2**-n)**i
# lower bound (exclusive):  (2**2**-n)**(i-1)

An important property falls out of this. Schema n has half the resolution of schema n+1, so a histogram at n+1 can be converted to n by merging neighboring buckets. This is why standard schemas are mutually mergeable, and it's the basis for queries that sum histograms across multiple instances. The problem in classic histograms — where two services using different bucket boundaries couldn't be combined — disappears here.

On the instrumentation side, you don't pick the schema directly; you specify a growth factor. That's the Go client's NativeHistogramBucketFactor, and the comment in the client_golang source gives the exact figures — a generally good tradeoff between cost and accuracy is 1.1 (each bucket at most 10% wider than the previous one), which works out to 8 buckets per power-of-two interval (8 between 1 and 2, 8 between 2 and 4, 8 between 4 and 8, and so on). The actual factor is computed as 2^(2^-n), where n is chosen as the largest integer between -4 and 8 that yields a factor no greater than the one specified. So the smallest possible factor is about 1.00271 (= 2^(2^-8)).

The area near zero is handled separately. Exponential buckets approach zero infinitely, so any observation whose absolute value is at or below a threshold is collected into a "zero bucket." The Go client's default threshold is DefNativeHistogramZeroThreshold = 2.938735877055719e-39.

And storage is a sparse representation — empty buckets are (nearly) free. That's why claiming to "cover the entire float64 range" doesn't bankrupt you. However wide the covered range is, only the buckets that are actually filled cost anything.

What Stable Doesn't Guarantee

This is where the real substance of this post begins. The stable declaration means the way the server handles this data type won't break before v4 — it does not mean "you can just turn it on now." The spec itself attaches caveats right after the stable declaration.

You still have to turn it on explicitly. Scraping must be enabled with the scrape_native_histograms setting, and Remote-Write transmission has to be turned on separately with send_native_histograms. In 3.8, the legacy feature flag only had the effect of setting this config to true; starting with 3.9, the flag is a complete no-op, so you must set the config explicitly. A parenthetical in the spec tells you the roadmap — starting with v4, both settings default to true. So right now we're in a middle stretch: "stable but opt-in," with v4 as "the default."

Exposition is protobuf-only. This is the thing that trips people up most often in practice. The spec is explicit that the classic Prometheus text format has not been extended for native histograms and there's no plan to. OpenMetrics text format support has a proposal document, but its implementation status remains Not implemented. The PR for OpenMetrics protobuf support (OpenMetrics#256) was closed without merging on March 19, 2026 — and the reason is interesting: it's effectively a branding cleanup. The decision was that since the Prometheus protobuf exposition format already supports native histograms and everything else, that format becomes the official protobuf format, while OpenMetrics is treated as referring to the text-based format. To sum up, as of July 2026, there's exactly one path to exposing native histograms: the classic Prometheus protobuf format.

Client libraries haven't caught up yet. This one is worth checking for yourself. The latest client_golang release is v1.23.2 (September 5, 2025), and the comment on HistogramOpts still reads, word for word:

NOTE: Native Histograms are still an experimental feature. Their behavior might still change without a major version bump. Subsequently, all NativeHistogram... options here might still change their behavior or name (or might completely disappear) without a major version bump.

You could argue v1.23.2 predates 3.8.0 (December 2025) and the wording is just stale — and in fact the same sentence is still present on main. The point is that the subject of the stable declaration is the server. The server's data model and its storage/query behavior are stable, but the instrumentation library's option API is not documented as carrying the same guarantee.

Python is simpler — the issue tracking native histogram implementation in client_python is still open. This is one of the motivations the text-format proposal document cites explicitly: some libraries, like the Python client, don't want to add a protobuf dependency just to expose native histograms.

Kubernetes is alpha. Native histograms landed in Kubernetes component metrics starting with v1.36 (released April 22, 2026), and the NativeHistograms feature gate's status is alpha, default false. It applies to kube-apiserver, kube-controller-manager, kube-scheduler, kubelet, and kube-proxy, and can be turned on per component. When enabled, it's a dual exposition scheme exposing both classic and native simultaneously, and the request's Accept header decides which one you get.

The Protobuf Scrape Cost — the Problem That Originally Blocked Stable

There's an interesting bit of history here. Turning on scrape_native_histograms changes content negotiation so that scrape config prefers protobuf. As the spec states, when no default is specified, the effective priority order becomes:

[ PrometheusProto, OpenMetricsText1.0.0, OpenMetricsText0.0.1, PrometheusText0.0.4 ]

The problem was that this applied to every target under that scrape config. Scraping a handful of native histograms meant flipping your entire scrape over to protobuf. Issue #14668 tackled this head-on, and its diagnosis was — protobuf parsing generates a lot of memory churn, which mostly shows up as increased GC CPU usage, and depending on usage patterns CPU can rise significantly; in real-world cases a 2x increase has been observed. And the same issue body states, in bold:

(Or in other words: This issue needs to get resolved to declare native histograms a stable feature.)

In other words, this performance problem was a precondition for the stable declaration. The issue was closed as completed on February 13, 2025, and the stable declaration followed in December 2025. In discussion before it closed, a maintainer reported that in realistic benchmarks after optimization, it was 3x faster than the text parser and used 10% less allocated memory.

There's something to be honest about here. The spec doc still carries a note saying that scraping via a text-based format generally uses far fewer resources than protobuf, and links this same issue as its evidence — but that issue is closed as completed. The doc's note looks like it reflects the pre-optimization state and is stale. So it would be an overreach, given current sources, to say either "protobuf scraping is still 2x more expensive" or "this is now fully resolved." You have no choice but to measure CPU in your own environment. Compare the CPU of both the scraped process and Prometheus itself before and after turning it on.

There is one practical legacy this issue left behind, though: tools for narrowing exposition scope. Setting scrape_protocols explicitly lets you force specific targets onto non-protobuf formats even when scrape_native_histograms is on. And 3.13 added internal labels for per-target overrides via relabeling — __scrape_native_histograms__ and __always_scrape_classic_histograms__ (#18929), and __convert_classic_histograms_to_nhcb__ (#18840). That means you can turn native histograms on precisely for the targets that expose them, without splitting your scrape config.

NHCB — a Path That Doesn't Touch Instrumentation Code

Re-instrumenting every classic histogram to native from scratch is a big job. So there's a middle path: schema -53, a native histogram with custom bucket boundaries (NHCB).

Turning on the convert_classic_histograms_to_nhcb scrape option makes Prometheus convert classic histograms into NHCB at ingestion time. You leave the instrumentation code untouched and get the storage efficiency anyway. In the spec's words, NHCB's main advantage is that storage cost is generally much cheaper, and in particular the marginal cost of adding a bucket is low, so even classic histograms with many buckets become affordable to ingest.

The tradeoff, though, is clear. NHCB is fundamentally limited in its ability to merge with other histograms. The spec warns as follows — automatic reconciliation between different bucket layouts only keeps the boundaries that match exactly, so if most of the boundaries line up you get a usable result, but if bucket layouts diverge arbitrarily you can end up with a histogram where no boundary matches, leaving only a single overflow bucket. That's why the spec recommends using schema -53 only as an informed decision for specific use cases. If you push a codebase with inconsistent bucket boundaries across services entirely into NHCB and run aggregate queries, you'll quietly get useless results.

An Honest Look at the Tradeoffs — Why Migration Is Hard

The spec's migration section boils the problem down to three points. This is the real difficulty of adopting this feature.

First, queries work differently. In most cases the changes are minimal and intuitive, but the spec's own account is that there are tricky edge cases that make reliable automatic conversion hard. In other words, you have to move dashboards and alerting rules by hand.

Second, classic and native don't aggregate with each other. This is the most painful part. If you flip from classic to native at some point in time, it's hard to build a dashboard that straddles that cutover, and any range vector that spans the cutover is necessarily incomplete — pick classic and you only get the earlier data, pick native and you only get the later data. The spec's own example is concrete.

histogram_quantile(0.9, rate(rpc_duration_seconds[1d]))

This query, computing the 90th-percentile latency over a day, only covers a shorter window than that if you haven't been collecting native histograms for at least a full day. So the spec's recommendation is conservative — collect classic and native in parallel for a while. If you've run them in parallel for a month, any dashboard that doesn't look back further than a month can just switch over without worrying about the cutover point. Parallel collection is turned on with the always_scrape_classic_histograms option. Classic ends up as suffixed timeseries and native as a single timeseries under the plain name, so they don't collide in the TSDB. Of course, you pay for both the whole time.

Third, you can't pin arbitrary bucket boundaries. This can be decisive for teams doing SLOs. A classic histogram can be custom-designed with a boundary planted exactly where you care about it — for instance, if "percentage of responses within 300ms" is your SLO, pinning a boundary at 300ms makes that percentage an exact value rather than an estimate. A native histogram on a standard schema has high resolution but can't put a boundary at an arbitrary value. The Grafana Mimir docs flag the same point as a drawback — there's no way to configure arbitrary bucket boundaries such as thresholds that matter for SLO definitions, and the fraction of observations above or below a given threshold generally has to be estimated via interpolation. The spec is candid that in these cases "the user experience with native histograms may in fact get worse," and offers the option of simply leaving that particular histogram unmigrated as a solution.

One more thing on top of this. Because buckets are created dynamically as they're first filled, a wider-than-expected distribution creates more buckets than expected and uses more memory. The spec even warns that if observations can be manipulated externally, this can become a denial-of-service vector via memory exhaustion. Yet the Go client's NativeHistogramMaxBucketNumber defaults to 0, i.e. unlimited (the spec itself leaves this as a TODO). A commonly used value is 160, which matches the default for OTel exponential histograms. You can also defend on the collection side — the scrape config's native_histogram_bucket_limit (a cap on the number of buckets per histogram) and native_histogram_min_bucket_factor (a floor on the growth factor), both of which reduce resolution to fit when exceeded. Note, though, that if NHCB exceeds the bucket limit it can't reduce resolution, so the scrape simply fails.

So, Should You Turn It On?

Worth turning on now

  • You're using classic histograms with many (dozens of) buckets, and cardinality is already hurting because of label partitioning. Since the savings scale with the original bucket count, this is where it pays off the most.
  • Observations whose range is hard to know in advance, like latency distributions. Not having to guess bucket boundaries up front is a real, practical gain here.
  • You're instrumenting in Go and planning to upgrade your server to the 3.13 LTS. This is the smoothest path on your current stack right now.
  • You can't touch instrumentation code but want to cut storage cost — here you have the option of turning on NHCB conversion alone. Just understand the merge limitations first.

Not yet

  • Histograms whose bucket boundaries are pinned exactly to your SLO thresholds. The moment you switch to interpolated estimates, the nature of the SLO calculation changes. Just leave these classic.
  • Anywhere instrumented with the Python client. It's still unimplemented, so there's no choice to make yet.
  • Histograms with only a handful of buckets. There's little to gain, while you still pay the full migration cost.
  • Setups where most scrape targets don't expose native histograms but the scrape config would flip everything to protobuf as a unit. Narrow the scope with 3.13's per-target relabeling, or control it with scrape_protocols.
  • Anywhere the goal is Kubernetes component metrics. It's disabled by default in the 1.36 alpha, so turning on an alpha feature gate in a production cluster is a separate decision you'd need to make on its own.

Closing

To sum up: native histograms really are stable now (3.8.0, December 2025), 3.13 is the first LTS to carry this feature as stable (July 1, 2026), and the 3.5 LTS ends July 31. The data model is genuinely good — N+2 timeseries become 1, you no longer have to guess bucket boundaries, and standard schemas merge with each other.

But you have to read the subject of the word "stable" precisely. It means the way the server handles this data type won't break before v4 — it does not mean the ecosystem is ready. Exposition is still protobuf-only, the Go client still documents its own options as experimental, Python is unimplemented, and Kubernetes is alpha. The direction toward defaults flipping on in v4 is clear, but anyone crossing this in-between stretch right now has to check these gaps for themselves.

So the practical order of operations is this: upgrading to 3.13 is something you have to do anyway because of July 31, so do that first, and treat native histograms as a separate decision. Then pick just a handful of your most cardinality-painful histograms and start them with parallel collection, measure the CPU impact of the protobuf switch yourself, and set the parallel-collection period long enough to give yourself time to move dashboards over. This is a feature that took three years to go stable. There's no reason you need to migrate everything in two weeks.

References