<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged prometheus</title>
<link>https://coderlegion.com/tag/prometheus</link>
<description>Powered by Question2Answer</description>
<item>
<title>Production-Ready Go Scraper with Prometheus - Complete Implementation Building a scraper</title>
<link>https://coderlegion.com/29672/production-ready-go-scraper-with-prometheus-complete-implementation-building-scraper</link>
<description>&lt;p&gt;Building a scraper that works is easy. Building a scraper that you can observe in production is senior work.&lt;/p&gt;
&lt;p&gt;I added Prometheus to my Go Jobs Tracker and here's the full working implementation.&lt;/p&gt;
&lt;p&gt;PART 1: METRICS DEFINITION&lt;/p&gt;
&lt;p&gt;Create file metrics/metrics.go:&lt;/p&gt;
&lt;p&gt;package metrics&lt;/p&gt;
&lt;p&gt;import (&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;github.com/prometheus/client_golang/prometheus&quot;
&quot;github.com/prometheus/client_golang/prometheus/promauto&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;)&lt;/p&gt;
&lt;p&gt;var (&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;JobsScraped = promauto.NewCounterVec(
    prometheus.CounterOpts{
        Name: &quot;jobs_scraped_total&quot;,
        Help: &quot;Total number of jobs scraped per engine and status&quot;,
    },
    []string{&quot;engine&quot;, &quot;status&quot;},
)

ScrapeDuration = promauto.NewHistogramVec(
    prometheus.HistogramOpts{
        Name: &quot;scrape_duration_seconds&quot;,
        Help: &quot;Duration of scrape jobs&quot;,
        Buckets: prometheus.DefBuckets,
    },
    []string{&quot;engine&quot;},
)

ErrorsTotal = promauto.NewCounterVec(
    prometheus.CounterOpts{
        Name: &quot;scraper_errors_total&quot;,
        Help: &quot;Total errors by type&quot;,
    },
    []string{&quot;type&quot;},
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;)&lt;/p&gt;
&lt;p&gt;PART 2: MAIN.GO INTEGRATION - 100% WORKING&lt;/p&gt;
&lt;p&gt;package main&lt;/p&gt;
&lt;p&gt;import (&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;net/http&quot;
&quot;time&quot;
&quot;github.com/prometheus/client_golang/prometheus/promhttp&quot;
&quot;github.com/Aureliopires186/go-jobs-tracker/metrics&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;)&lt;/p&gt;
&lt;p&gt;func main() {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;go func() {
    http.Handle(&quot;/metrics&quot;, promhttp.Handler())
    http.HandleFunc(&quot;/healthz&quot;, func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(200)
        w.Write([]byte(&quot;ok&quot;))
    })
    http.HandleFunc(&quot;/&quot;, func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte(&quot;go-jobs-tracker running. check /metrics and /healthz&quot;))
    })
    http.ListenAndServe(&quot;:2112&quot;, nil)
}()

start := time.Now()

// Simulate your scraping logic here
err := ScrapeGoogleJobs()

duration := time.Since(start)

if err != nil {
    metrics.ErrorsTotal.WithLabelValues(&quot;scrape_failed&quot;).Inc()
} else {
    metrics.JobsScraped.WithLabelValues(&quot;google_jobs&quot;, &quot;success&quot;).Inc()
    metrics.ScrapeDuration.WithLabelValues(&quot;google_jobs&quot;).Observe(duration.Seconds())
}

select {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;func ScrapeGoogleJobs() error {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// your logic
return nil
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;PART 3: GO.MOD DEPENDENCY&lt;/p&gt;
&lt;p&gt;Run:&lt;br&gt;
go get github.com/prometheus/client_golang@v1.19.0&lt;br&gt;
go mod tidy&lt;/p&gt;
&lt;p&gt;PART 4: HOW TO TEST IT LOCALLY&lt;/p&gt;
&lt;p&gt;Terminal 1:&lt;br&gt;
go run main.go&lt;/p&gt;
&lt;p&gt;Terminal 2:&lt;br&gt;
curl &lt;a rel=&quot;nofollow&quot; href=&quot;http://localhost:2112/metrics&quot;&gt;http://localhost:2112/metrics&lt;/a&gt;&lt;br&gt;
curl &lt;a rel=&quot;nofollow&quot; href=&quot;http://localhost:2112/healthz&quot;&gt;http://localhost:2112/healthz&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;You will see:&lt;br&gt;
jobs_scraped_total&lt;br&gt;
scrape_duration_seconds&lt;br&gt;
scraper_errors_total&lt;/p&gt;
&lt;p&gt;PART 5: PROMETHEUS CONFIG&lt;/p&gt;
&lt;p&gt;prometheus.yml&lt;/p&gt;
&lt;p&gt;global:&lt;br&gt;
  scrape_interval: 15s&lt;br&gt;
scrape_configs:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- job_name: 'go-jobs-tracker'
static_configs:
        - targets: ['localhost:2112']
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This setup is production ready, K8s ready, and used by real companies.&lt;/p&gt;
&lt;p&gt;No more blind scraping.&lt;/p&gt;
&lt;p&gt;•Author :Pires Aurélio &lt;/p&gt;
&lt;p&gt;•Go Developer &lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/29672/production-ready-go-scraper-with-prometheus-complete-implementation-building-scraper</guid>
<pubDate>Sat, 03 Oct 2026 23:53:01 +0000</pubDate>
</item>
<item>
<title>How to Deploy an Open Source LLM Reliably (on Kubernetes)</title>
<link>https://coderlegion.com/16480/how-to-deploy-an-open-source-llm-reliably-on-kubernetes</link>
<description>&lt;p&gt;Everyone is talking about LLMs, but very few people actually &lt;em&gt;run&lt;/em&gt; one.&lt;/p&gt;
&lt;p&gt;Most developers rely on APIs from OpenAI or Anthropic. It works great until you start thinking about cost, control, or what happens when that API isn’t available. That’s when a more interesting question shows up: &lt;em&gt;can you run your own LLM reliably?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Not just something that works once on your laptop, but something that behaves like real infrastructure. Something observable, restartable, and usable through an actual interface. That’s what this guide is about. We’ll deploy an open-source model (&lt;strong&gt;TinyLlama&lt;/strong&gt;) on Kubernetes using Kind, serve it using Ollama, build a simple chatbot UI, and monitor everything using Prometheus and Grafana. By the end, you won’t just have a working setup you’ll understand how LLMs behave as systems.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;understanding-what-we-re-actually-building&quot;&gt;Understanding What We’re Actually Building&lt;/h3&gt;
&lt;p&gt;Before running commands, This setup is a small but complete system: a Kubernetes cluster running an LLM service, connected to a UI, with a monitoring layer observing everything. When you send a message in the chatbot, it flows through multiple layers before coming back as a response.&lt;/p&gt;
&lt;p&gt;User → Chat UI → LLM API → Kubernetes → Metrics → Grafana&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.hashnode.com/uploads/covers/682321f73a70857789a294b1/8e7c7e91-ec45-4065-944f-95f5d856b3c1.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;That flow is the entire point. Once you understand this, you stop seeing LLMs as magic models and start seeing them as workloads running on infrastructure.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-not-just-use-gemini-or-any-commercial-llm-&quot;&gt;Why Not Just Use Gemini or Any Commercial LLM?&lt;/h3&gt;
&lt;p&gt;At this point, a fair question comes up. Why go through all this effort setting up Kubernetes, deploying an LLM, adding monitoring when you can just open a browser, go to Google Gemini or use APIs from OpenAI, and get answers in seconds?And honestly, for many use cases… you should. Commercial LLMs are fast, powerful, and incredibly easy to use. They handle scaling, reliability, and updates for you. If your goal is to get high-quality answers quickly, they are the best choice.But that’s not the whole picture. Running your own LLM changes what you control.When it runs inside your infrastructure, there’s no external dependency, no per-request cost, and full control over data and behavior. The trade-off is simple: you gain control, but you take on responsibility.&lt;/p&gt;
&lt;p&gt;This is where open-source LLMs make sense especially for internal tools, privacy-sensitive use cases, or cost-heavy workloads at scale. But smaller models like TinyLlama still lag behind commercial models in reasoning and depth. So this isn’t about replacing commercial LLMs. It’s about understanding when to own the system—and when not to.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Aspect &lt;/th&gt;&lt;th&gt;Open Source LLM (TinyLlama on K8s) &lt;/th&gt;&lt;th&gt;Commercial LLM (Gemini / OpenAI / Anthropic)&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Setup &lt;/td&gt;&lt;td&gt;Complex (infra + deployment) &lt;/td&gt;&lt;td&gt;Instant (UI / API)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Cost &lt;/td&gt;&lt;td&gt;No per-request cost (infra only) &lt;/td&gt;&lt;td&gt;Pay-per-use&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Latency &lt;/td&gt;&lt;td&gt;Very low (local) &lt;/td&gt;&lt;td&gt;Depends on network/API&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Quality &lt;/td&gt;&lt;td&gt;Moderate &lt;/td&gt;&lt;td&gt;High&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Control &lt;/td&gt;&lt;td&gt;Full control over system &lt;/td&gt;&lt;td&gt;Limited&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Privacy &lt;/td&gt;&lt;td&gt;Fully local &lt;/td&gt;&lt;td&gt;Data leaves your system&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Reliability &lt;/td&gt;&lt;td&gt;Depends on your setup &lt;/td&gt;&lt;td&gt;Managed by provider&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The real decision isn’t which one is better it’s about which trade-offs you’re willing to make for your use case.&lt;/p&gt;
&lt;h3 id=&quot;clone-the-project-start-here-&quot;&gt;Clone the Project (Start Here)&lt;/h3&gt;
&lt;p&gt;Before we go deep into Kubernetes, LLMs, or observability, let’s get the system running locally. Everything in this guide is based on this repo: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Ayushmore1214?utm_source=chatgpt.com&quot;&gt;Clone the repository&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git clone https://github.com/Ayushmore1214/llm-k8s-deployment.git
cd llm-k8s-deployment
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;starting-clean&quot;&gt;Starting Clean&lt;/h3&gt;
&lt;p&gt;A lot of Kubernetes frustration comes from leftover state. Old configs, broken kubeconfigs, or previously installed clusters can silently interfere with your setup. So instead of debugging weird issues later, we start clean.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo rm -rf /etc/rancher
unset KUBECONFIG
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we install and create a cluster using Kind. Kind is perfect here because it runs Kubernetes inside Docker, making it fast and reproducible without any cloud dependency.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kind create cluster --name drdroid-llm
kind export kubeconfig --name drdroid-llm
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At this point, you already have something powerful: a working Kubernetes cluster on your machine. Everything after this builds on top of it.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;deploying-the-llm&quot;&gt;Deploying the LLM&lt;/h3&gt;
&lt;p&gt;Now we deploy the actual LLM infrastructure using a Kubernetes manifest.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl apply -f llm-stack.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This single command creates a deployment to keep the model running, a service to expose it inside the cluster, and the container configuration that defines how the model behaves.&lt;/p&gt;
&lt;p&gt;You can watch Kubernetes doing its job in real time:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl get pods -w
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once the pod is running, we still need to load the model itself. Until this step, the system is alive—but empty.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl exec -it $(kubectl get pods -l app=ollama -o name) -- ollama pull tinyllama
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the model is actually usable. This is the moment where infrastructure turns into intelligence.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;observability-stack&quot;&gt;Observability Stack&lt;/h3&gt;
&lt;p&gt;Most guides stop after it works. That’s not enough. If you can’t see what your system is doing, you don’t really control it. That’s why we add observability using Prometheus and Grafana.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;helm install obs prometheus-community/kube-prometheus-stack \
  --set prometheus.prometheusSpec.resources.requests.memory=300Mi \
  --set grafana.adminPassword=admin
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This adds a full monitoring layer to your cluster. Prometheus collects metrics, and Grafana lets you visualize them. Instead of guessing what’s happening, you can now see CPU usage, memory consumption, and pod health in real time.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;accessing-the-system&quot;&gt;Accessing the System&lt;/h3&gt;
&lt;p&gt;By default, Kubernetes services are internal. To interact with them locally, we forward ports.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl port-forward svc/ollama-service 11434:11434
kubectl port-forward svc/obs-grafana 3000:80
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now your LLM API is available on port 11434, and Grafana is accessible on port 3000. At this point, everything is running but it’s still not very interactive. That’s where the UI comes in.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;adding-a-chatbot-ui&quot;&gt;Adding a Chatbot UI&lt;/h3&gt;
&lt;p&gt;Instead of calling APIs manually, we build a simple chatbot interface using Streamlit.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip install streamlit requests
python3 -m streamlit run app.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Open the UI in your browser and start asking questions. Things like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;“Which model are you?”&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;“What’s your latest knowledge?”&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;“Explain Kubernetes simply”&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is where the system finally feels complete. You’re interacting with your own LLM, running on your own infrastructure.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;watching-the-system-under-load&quot;&gt;Watching the System Under Load&lt;/h3&gt;
&lt;p&gt;Now open Grafana at &lt;code&gt;http://localhost:3000&lt;/code&gt; and log in using admin/admin. Navigate to the Kubernetes dashboards and select the Ollama pod.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.hashnode.com/uploads/covers/682321f73a70857789a294b1/cef2ce45-b418-4fed-a1d5-b1ee8c7ac844.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Go back to your chatbot and start sending prompts. You’ll notice something interesting immediately. Every request creates visible activity—CPU spikes, memory usage increases, and resource patterns start forming. The model isn’t just answering questions. It’s consuming compute. This is the shift in understanding that matters. LLMs are not just software. They are &lt;strong&gt;workloads&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-this-demo-actually-teaches-you-&quot;&gt;What This Demo Actually Teaches You ?&lt;/h3&gt;
&lt;p&gt;This project might look simple, but it introduces some core ideas that show up in real systems:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Kubernetes ensures your service stays alive even if it crashes&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;LLM inference consumes real, measurable resources&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Observability is what turns debugging into something practical&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once you see these pieces working together, the abstraction breaks in a good way. You stop treating LLMs as black boxes and start understanding how they behave under the hood.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Running an LLM locally is cool. Running it on Kubernetes with a full observability stack? That’s how you actually learn how AI behaves in the real world. This setup is just the beginning. It’s containerized, it’s orchestrated, and it’s observable this is the exact pattern that powers production AI at scale. Once you see the CPU spikes in Grafana and manage your own cluster, you stop looking at AI as a magic API and start seeing it as &lt;strong&gt;workload engineering.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The best way to learn isn’t by reading it’s by breaking stuff.&lt;/strong&gt; I built this project to be a playground. Go ahead: delete a pod, change things in &lt;code&gt;llm-stack.yaml&lt;/code&gt;, or try to crash the model with a massive prompt. See if your Grafana dashboard catches it.&lt;/p&gt;
&lt;p&gt;If this guide helped you make sense of the chaos, give the &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Ayushmore1214/llm-k8s-deployment.git&quot;&gt;repo&lt;/a&gt; a ⭐, and let’s connect on &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linkedin.com/in/ayush-more-3b4154341&quot;&gt;LinkedIn&lt;/a&gt;. I’m always looking for people who want to build, ship, and break things. Thank You for Reading!!!&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/16480/how-to-deploy-an-open-source-llm-reliably-on-kubernetes</guid>
<pubDate>Sun, 03 May 2026 07:06:11 +0000</pubDate>
</item>
<item>
<title>Prometheus 3.0 and OpenTelemetry: Native OTLP Support Explained</title>
<link>https://coderlegion.com/15636/prometheus-3-0-and-opentelemetry-native-otlp-support-explained</link>
<description>
&lt;p&gt;Seven years is a long time in observability. Since Prometheus 2.0 landed in 2017, the ecosystem has been transformed by cloud-native adoption, the rise of distributed tracing, and the emergence of OpenTelemetry as the de facto standard for instrumentation. Prometheus 3.0, released in November 2024, is the project&amp;rsquo;s answer to that transformation &amp;mdash; and its most significant change is the native ability to ingest OpenTelemetry metrics directly, without an intermediary collector standing in the way.&lt;/p&gt;



&lt;p&gt;This article goes deep on what Prometheus 3.0 actually changes for platform engineers and cloud architects who are running &amp;mdash; or planning to run &amp;mdash; OTel-instrumented workloads alongside Prometheus-based monitoring stacks. We will cover the native OTLP ingestion endpoint, UTF-8 metric name support, Remote Write 2.0, migration considerations, and the architectural patterns that still make sense even when native OTLP is available.&lt;/p&gt;



&lt;h2 class=&quot;wp-block-heading&quot;&gt;What Changed in Prometheus 3.0: The OTel-Relevant Picture&lt;/h2&gt;



&lt;p&gt;Prometheus 3.0 ships a substantial set of changes. Not all of them are equally relevant to OpenTelemetry integration, so let&amp;rsquo;s focus on what actually moves the needle for OTel users before diving into each area in detail.&lt;/p&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;Native OTLP Ingestion&lt;/h3&gt;



&lt;p&gt;The flagship feature: Prometheus 3.0 ships with a built-in OTLP receiver that exposes an HTTP endpoint accepting metrics in the OpenTelemetry Protocol format. Applications instrumented with any OTel SDK can now push metrics directly to Prometheus without routing through an OpenTelemetry Collector. This is not a sidecar, not a plugin, not an external adapter &amp;mdash; it is a first-class endpoint in the Prometheus binary itself.&lt;/p&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;UTF-8 Metric Names&lt;/h3&gt;



&lt;p&gt;Prometheus historically restricted metric names to &lt;code&gt;[a-zA-Z_:][a-zA-Z0-9_:]*&lt;/code&gt;. OpenTelemetry uses dots and slashes in metric names by convention &amp;mdash; &lt;code&gt;http.server.request.duration&lt;/code&gt; is a canonical OTel metric name. Prometheus 3.0 lifts this restriction and supports arbitrary UTF-8 characters in metric names and label names, which is the single most important compatibility change for OTel interoperability.&lt;/p&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;Remote Write 2.0&lt;/h3&gt;

&lt;h3 class=&quot;wp-block-heading&quot;&gt;Remote Write 2.0&lt;/h3&gt;



&lt;p&gt;Remote Write 2.0 replaces the original protocol with a more efficient encoding based on protobuf, adds native histogram support in the wire format, and reduces bandwidth consumption significantly for large-scale deployments. If you are federating metrics to Thanos, Mimir, or Cortex, this matters for operational cost.&lt;/p&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;New UI&lt;/h3&gt;



&lt;p&gt;The Prometheus web UI has been completely rewritten. The new UI uses React, supports metric metadata exploration, and provides a significantly improved query-building experience. This is a quality-of-life improvement rather than an architectural change, but it reduces the dependency on external tools like Grafana for ad-hoc investigation.&lt;/p&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;Breaking Changes Summary&lt;/h3&gt;



&lt;p&gt;Prometheus 3.0 removes several features that were deprecated in 2.x. The most operationally significant are: removal of the &lt;code&gt;--web.enable-admin-api&lt;/code&gt; deprecated flag path, removal of certain legacy storage format options, changes to default scrape timeouts, and stricter validation of configuration that was previously silently accepted. We cover a migration checklist later in this article.&lt;/p&gt;



&lt;h2 class=&quot;wp-block-heading&quot;&gt;The OTLP Receiver: How It Works and What It Accepts&lt;/h2&gt;



&lt;p&gt;The OTLP receiver in Prometheus 3.0 is implemented as an optional feature that must be explicitly enabled. Once enabled, it exposes an HTTP endpoint at &lt;code&gt;/api/v1/otlp/v1/metrics&lt;/code&gt; that accepts protobuf-encoded OTLP ExportMetricsServiceRequest payloads &amp;mdash; the same wire format used by the OpenTelemetry Collector&amp;rsquo;s OTLP exporter.&lt;/p&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;What It Accepts (and What It Does Not)&lt;/h3&gt;



&lt;p&gt;This is critical to understand before you architect around native OTLP ingestion: &lt;strong&gt;Prometheus 3.0 OTLP support is metrics-only&lt;/strong&gt;. It does not accept traces or logs. OTLP is a unified protocol covering all three signals, but Prometheus is a metrics store &amp;mdash; the receiver handles only the metrics portion of the OTLP specification.&lt;/p&gt;



&lt;p&gt;Supported metric types in the OTLP receiver:&lt;/p&gt;



&lt;ul class=&quot;wp-block-list&quot;&gt;&lt;li&gt;&lt;strong&gt;Gauge&lt;/strong&gt; &amp;mdash; maps directly to a Prometheus Gauge&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Sum (monotonic)&lt;/strong&gt; &amp;mdash; maps to a Prometheus Counter&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Sum (non-monotonic)&lt;/strong&gt; &amp;mdash; maps to a Prometheus Gauge&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Histogram (explicit bucket)&lt;/strong&gt; &amp;mdash; maps to a Prometheus Histogram&lt;/li&gt;&lt;li&gt;&lt;strong&gt;ExponentialHistogram&lt;/strong&gt; &amp;mdash; maps to Prometheus Native Histograms (a 3.0 feature)&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Summary&lt;/strong&gt; &amp;mdash; maps to a Prometheus Summary&lt;/li&gt;&lt;/ul&gt;



&lt;p&gt;Resource attributes from the OTLP payload &amp;mdash; things like &lt;code&gt;service.name&lt;/code&gt;, &lt;code&gt;k8s.pod.name&lt;/code&gt;, &lt;code&gt;cloud.region&lt;/code&gt; &amp;mdash; are converted to Prometheus labels. This conversion is configurable, and by default Prometheus applies a promotion strategy that converts the most common resource attributes to labels while discarding ones that would create extremely high cardinality.&lt;/p&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;Enabling the OTLP Receiver&lt;/h3&gt;



&lt;p&gt;Enabling native OTLP ingestion requires two things: a feature flag and a configuration block in &lt;code&gt;prometheus.yml&lt;/code&gt;.&lt;/p&gt;



&lt;p&gt;Start the Prometheus binary with the feature flag:&lt;/p&gt;



&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;prometheus \
  --config.file=/etc/prometheus/prometheus.yml \
  --enable-feature=otlp-write-receiver&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Then add the OTLP receiver configuration to your &lt;code&gt;prometheus.yml&lt;/code&gt;:&lt;/p&gt;



&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;# prometheus.yml
global:
  scrape_interval: 15s
  evaluation_interval: 15s

otlp:
  # Promote these OTLP resource attributes to Prometheus labels
  promote_resource_attributes:
    - service.name
    - service.namespace
    - service.instance.id
    - k8s.namespace.name
    - k8s.pod.name
    - k8s.node.name
    - cloud.region
    - deployment.environment&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;With this configuration, Prometheus will listen on port 9090 (default) and accept OTLP metrics at &lt;code&gt;http://&amp;lt;prometheus-host&amp;gt;:9090/api/v1/otlp/v1/metrics&lt;/code&gt;.&lt;/p&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;Resource Attribute Promotion Strategy&lt;/h3&gt;



&lt;p&gt;The &lt;code&gt;promote_resource_attributes&lt;/code&gt; list deserves careful thought. OTLP carries rich resource-level context &amp;mdash; every metric payload includes a ResourceMetrics object with attributes describing the source: service name, version, environment, Kubernetes pod, node, cluster, cloud provider details, and more. Prometheus labels are flat key-value pairs on each time series. Promoting too many resource attributes explodes cardinality; promoting too few loses important context.&lt;/p&gt;



&lt;p&gt;A pragmatic starting list for Kubernetes deployments:&lt;/p&gt;



&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;otlp:
  promote_resource_attributes:
    - service.name          # Critical: identifies the service
    - service.namespace     # Logical grouping
    - deployment.environment  # prod/staging/dev
    - k8s.namespace.name    # Kubernetes namespace
    - k8s.pod.name          # Pod-level cardinality &amp;mdash; consider omitting in high-scale
    - k8s.node.name         # Useful for infrastructure correlation&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Avoid blindly promoting &lt;code&gt;k8s.pod.name&lt;/code&gt; at scale &amp;mdash; in a cluster with thousands of short-lived pods, this creates significant cardinality pressure. Prefer &lt;code&gt;service.name&lt;/code&gt; and &lt;code&gt;service.namespace&lt;/code&gt; for most alerting use cases, reserving pod-level labels for debugging dashboards.&lt;/p&gt;



&lt;h2 class=&quot;wp-block-heading&quot;&gt;UTF-8 Metric Names: Why This Is the Real Game-Changer&lt;/h2&gt;



&lt;p&gt;To appreciate why UTF-8 metric name support matters so much, you need to understand the friction it eliminates. OpenTelemetry semantic conventions define metric names using dots as namespace separators. The canonical HTTP server duration metric is &lt;code&gt;http.server.request.duration&lt;/code&gt;. The canonical database query duration is &lt;code&gt;db.client.operation.duration&lt;/code&gt;. These names are standardized across languages and frameworks &amp;mdash; your Go service and your Java service and your Python service all emit the same metric name when instrumented with OTel.&lt;/p&gt;



&lt;p&gt;Prometheus 2.x could not store these names. The dots are illegal characters in Prometheus metric naming. Every OTel-to-Prometheus bridge &amp;mdash; the OpenTelemetry Collector&amp;rsquo;s Prometheus exporter, prom-client compatibility layers, the older &lt;code&gt;prometheusremotewrite&lt;/code&gt; exporter &amp;mdash; had to translate these names, typically by replacing dots with underscores: &lt;code&gt;http_server_request_duration&lt;/code&gt;.&lt;/p&gt;



&lt;p&gt;This translation is lossy and creates multiple problems:&lt;/p&gt;



&lt;ul class=&quot;wp-block-list&quot;&gt;&lt;li&gt;&lt;strong&gt;Name collisions&lt;/strong&gt;: &lt;code&gt;http.server.request_duration&lt;/code&gt; and &lt;code&gt;http.server.request.duration&lt;/code&gt; both become &lt;code&gt;http_server_request_duration&lt;/code&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Dashboard breakage&lt;/strong&gt;: Grafana dashboards built against OTel semantic conventions don&amp;rsquo;t work against translated Prometheus metrics without modification&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Cross-signal correlation&lt;/strong&gt;: Trace attributes use dot notation; when metric names differ, automated correlation tools lose the thread&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Vendor lock-in pressure&lt;/strong&gt;: Teams end up with separate naming conventions for &amp;ldquo;Prometheus metrics&amp;rdquo; vs &amp;ldquo;OTel metrics&amp;rdquo; and maintain both&lt;/li&gt;&lt;/ul&gt;



&lt;p&gt;Prometheus 3.0 with UTF-8 support stores &lt;code&gt;http.server.request.duration&lt;/code&gt; natively. No translation. No collision. The metric name you instrument with is the metric name you query.&lt;/p&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;Enabling UTF-8 Metric Names&lt;/h3&gt;



&lt;p&gt;UTF-8 metric names require the &lt;code&gt;utf8-names&lt;/code&gt; feature flag:&lt;/p&gt;



&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;prometheus \
  --config.file=/etc/prometheus/prometheus.yml \
  --enable-feature=utf8-names \
  --enable-feature=otlp-write-receiver&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Once enabled, PromQL queries must use quoted metric names when the name contains characters outside the legacy character set:&lt;/p&gt;



&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;# Legacy metric name &amp;mdash; unquoted works fine
http_server_requests_total

# OTel metric name with dots &amp;mdash; requires quoting in PromQL
{&quot;__name__&quot;=&quot;http.server.request.duration&quot;}

# Or using the new PromQL syntax in Prometheus 3.0
http.server.request.duration{service_name=&quot;api-gateway&quot;}&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;The PromQL parser in Prometheus 3.0 has been updated to handle quoted metric names as a first-class construct. Grafana&amp;rsquo;s PromQL engine has also been updated to handle this syntax &amp;mdash; verify your Grafana version (10.3+ has full support) before deploying.&lt;/p&gt;



&lt;h2 class=&quot;wp-block-heading&quot;&gt;OTel SDK to Prometheus 3.0 Directly: No Collector Required&lt;/h2&gt;



&lt;p&gt;For teams that only need to get application metrics into Prometheus, native OTLP ingestion enables a dramatically simpler architecture. Here&amp;rsquo;s what it looks like with different OTel SDKs.&lt;/p&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;Go (OpenTelemetry SDK)&lt;/h3&gt;



&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;package main

import (
    &quot;context&quot;
    &quot;time&quot;

    &quot;go.opentelemetry.io/otel&quot;
    &quot;go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp&quot;
    &quot;go.opentelemetry.io/otel/sdk/metric&quot;
    &quot;go.opentelemetry.io/otel/sdk/resource&quot;
    semconv &quot;go.opentelemetry.io/otel/semconv/v1.26.0&quot;
)

func initMetrics(ctx context.Context) (*metric.MeterProvider, error) {
    res, err := resource.New(ctx,
        resource.WithAttributes(
            semconv.ServiceName(&quot;my-api&quot;),
            semconv.ServiceNamespace(&quot;platform&quot;),
            semconv.DeploymentEnvironment(&quot;production&quot;),
        ),
    )
    if err != nil {
        return nil, err
    }

    // Point directly at Prometheus 3.0 OTLP endpoint
    exporter, err := otlpmetrichttp.New(ctx,
        otlpmetrichttp.WithEndpoint(&quot;prometheus:9090&quot;),
        otlpmetrichttp.WithURLPath(&quot;/api/v1/otlp/v1/metrics&quot;),
        otlpmetrichttp.WithInsecure(), // Use WithTLSClientConfig for production
    )
    if err != nil {
        return nil, err
    }

    provider := metric.NewMeterProvider(
        metric.WithResource(res),
        metric.WithReader(
            metric.NewPeriodicReader(exporter,
                metric.WithInterval(30*time.Second),
            ),
        ),
    )

    otel.SetMeterProvider(provider)
    return provider, nil
}&lt;/code&gt;&lt;/pre&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;Python (OpenTelemetry SDK)&lt;/h3&gt;



&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;from opentelemetry import metrics
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
from opentelemetry.sdk.resources import Resource, SERVICE_NAME, SERVICE_NAMESPACE

resource = Resource.create({
    SERVICE_NAME: &quot;my-api&quot;,
    SERVICE_NAMESPACE: &quot;platform&quot;,
    &quot;deployment.environment&quot;: &quot;production&quot;,
})

exporter = OTLPMetricExporter(
    endpoint=&quot;http://prometheus:9090/api/v1/otlp/v1/metrics&quot;,
)

reader = PeriodicExportingMetricReader(
    exporter,
    export_interval_millis=30_000,
)

provider = MeterProvider(resource=resource, metric_readers=[reader])
metrics.set_meter_provider(provider)

# Use the meter
meter = metrics.get_meter(&quot;my-api&quot;)
request_counter = meter.create_counter(
    name=&quot;http.server.request.count&quot;,
    description=&quot;Total HTTP server requests&quot;,
    unit=&quot;1&quot;,
)
request_duration = meter.create_histogram(
    name=&quot;http.server.request.duration&quot;,
    description=&quot;HTTP server request duration&quot;,
    unit=&quot;s&quot;,
)&lt;/code&gt;&lt;/pre&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;Java (OpenTelemetry SDK with Spring Boot)&lt;/h3&gt;



&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;# application.properties (Spring Boot with OTel auto-instrumentation)
otel.service.name=my-api
otel.resource.attributes=service.namespace=platform,deployment.environment=production

# Configure OTLP exporter to push directly to Prometheus
otel.metrics.exporter=otlp
otel.exporter.otlp.metrics.endpoint=http://prometheus:9090/api/v1/otlp/v1/metrics
otel.exporter.otlp.metrics.protocol=http/protobuf

# Export interval
otel.metric.export.interval=30000&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;With Spring Boot and the OTel Java agent, no code changes are required beyond configuration &amp;mdash; the agent instruments your HTTP server, database clients, and messaging systems automatically and pushes metrics using the names defined in OTel semantic conventions.&lt;/p&gt;



&lt;h2 class=&quot;wp-block-heading&quot;&gt;OTel Collector to Prometheus 3.0: When You Need the Intermediary&lt;/h2&gt;



&lt;p&gt;Native OTLP ingestion is compelling, but the OpenTelemetry Collector remains relevant for a significant set of use cases. Understanding when each pattern is appropriate is the core architectural decision you will face when adopting Prometheus 3.0 in an OTel environment.&lt;/p&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;Pattern 1: OTel Collector as Fan-Out Gateway&lt;/h3&gt;



&lt;p&gt;When you need to send metrics to multiple backends simultaneously &amp;mdash; Prometheus for alerting, a long-term store like Thanos for historical analysis, and a commercial observability platform for full-stack correlation &amp;mdash; the OTel Collector handles fan-out efficiently. Applications push once to the Collector; the Collector distributes to all backends.&lt;/p&gt;



&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;# otel-collector-config.yaml
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

processors:
  batch:
    timeout: 10s
    send_batch_size: 1000
  memory_limiter:
    check_interval: 1s
    limit_mib: 512

exporters:
  # Push to Prometheus 3.0 via OTLP
  otlphttp/prometheus:
    endpoint: http://prometheus:9090/api/v1/otlp
    tls:
      insecure: true

  # Fan-out to Thanos via remote_write
  prometheusremotewrite/thanos:
    endpoint: http://thanos-receive:10908/api/v1/receive
    resource_to_telemetry_conversion:
      enabled: true

  # Fan-out to commercial backend
  otlp/datadog:
    endpoint: &lt;a href=&quot;https://otel-intake.datadoghq.com&quot; rel=&quot;nofollow&quot;&gt;https://otel-intake.datadoghq.com&lt;/a&gt;
    headers:
      DD-API-KEY: &quot;${DD_API_KEY}&quot;

service:
  pipelines:
    metrics:
      receivers: [otlp]
      processors: [memory_limiter, batch]
      exporters: [otlphttp/prometheus, prometheusremotewrite/thanos, otlp/datadog]&lt;/code&gt;&lt;/pre&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;Pattern 2: Collector for Metric Transformation&lt;/h3&gt;



&lt;p&gt;The OTel Collector&amp;rsquo;s transform processor and metricstransform processor allow you to reshape metrics before they reach Prometheus: rename labels, add static attributes, filter out high-cardinality series, aggregate metrics to reduce storage cost, or apply unit conversions. These operations are not available in Prometheus&amp;rsquo;s native OTLP receiver.&lt;/p&gt;



&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;processors:
  transform/metrics:
    metric_statements:
      - context: metric
        statements:
          # Drop internal debug metrics
          - delete_matching_keys(attributes, &quot;internal.*&quot;)
          # Normalize environment label values
          - set(attributes[&quot;deployment.environment&quot;], &quot;prod&quot;)
            where attributes[&quot;deployment.environment&quot;] == &quot;production&quot;

  filter/drop_debug:
    metrics:
      exclude:
        match_type: regexp
        metric_names:
          - &quot;.*\.debug\..*&quot;
          - &quot;runtime\.go\.internal\..*&quot;

  metricstransform:
    transforms:
      # Rename a metric to match your existing Prometheus naming convention
      - include: http.server.request.duration
        action: update
        new_name: http_server_request_duration_seconds&lt;/code&gt;&lt;/pre&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;Pattern 3: Collector for Traces and Logs (Always Required)&lt;/h3&gt;



&lt;p&gt;If your architecture includes traces and logs alongside metrics &amp;mdash; and in 2025 it almost certainly does &amp;mdash; you need an OTel Collector regardless of what you do with metrics. Prometheus does not accept traces or logs. Jaeger, Tempo, and Loki all have their own ingestion protocols. The Collector is the universal routing layer for the three pillars of observability.&lt;/p&gt;



&lt;p&gt;In this architecture, it is usually simpler to route all three signals through the Collector and let it push metrics to Prometheus via OTLP or remote_write, rather than splitting metrics to go directly and everything else through the Collector.&lt;/p&gt;



&lt;h2 class=&quot;wp-block-heading&quot;&gt;When to Use Native OTLP vs. OTel Collector: Decision Framework&lt;/h2&gt;



&lt;figure class=&quot;wp-block-table&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Scenario&lt;/th&gt;&lt;th&gt;Native OTLP&lt;/th&gt;&lt;th&gt;OTel Collector&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Single metrics backend (Prometheus only)&lt;/td&gt;&lt;td&gt;Preferred&lt;/td&gt;&lt;td&gt;Overkill&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Multiple metrics backends&lt;/td&gt;&amp;lt;td&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/figure&gt;
</description>
<category>Other</category>
<guid isPermaLink="true">https://coderlegion.com/15636/prometheus-3-0-and-opentelemetry-native-otlp-support-explained</guid>
<pubDate>Mon, 27 Apr 2026 23:49:40 +0000</pubDate>
</item>
<item>
<title>Prometheus Scalability: High Cardinality and How to Fix It</title>
<link>https://coderlegion.com/15639/prometheus-scalability-high-cardinality-and-how-to-fix-it</link>
<description>
&lt;p&gt;Prometheus has become the de facto standard for metrics collection in cloud-native environments. Its pull-based model, powerful query language, and deep Kubernetes integration make it an obvious choice for platform teams. But as organizations scale &amp;mdash; more services, more replicas, more labels &amp;mdash; Prometheus starts showing cracks. Queries slow down, memory usage balloons, and what was once a reliable monitoring backbone becomes an operational liability. This article examines exactly why that happens and what you can do about it, from quick tactical fixes to full architectural overhauls.&lt;/p&gt;



&lt;h2 class=&quot;wp-block-heading&quot;&gt;The Cardinality Problem: Why It Kills Prometheus&lt;/h2&gt;



&lt;p&gt;Cardinality is the single most important concept to understand when troubleshooting Prometheus scalability. In the context of time series databases, cardinality refers to the total number of unique label combinations that exist across all your metrics. Every unique combination creates a distinct time series, and Prometheus must store, index, and query each of them independently.&lt;/p&gt;



&lt;p&gt;Consider a simple HTTP request counter: &lt;code&gt;http_requests_total&lt;/code&gt;. If you label it with &lt;code&gt;method&lt;/code&gt; (GET, POST, PUT, DELETE), &lt;code&gt;status_code&lt;/code&gt; (200, 201, 400, 404, 500, 503), and &lt;code&gt;endpoint&lt;/code&gt; (50 distinct API paths), you already have 4 &amp;times; 6 &amp;times; 50 = 1,200 time series from a single metric. Now add a &lt;code&gt;customer_id&lt;/code&gt; label with 10,000 distinct values. You have just created 12 million time series from one counter.&lt;/p&gt;



&lt;p&gt;This is the cardinality explosion pattern, and it is the most common cause of Prometheus degradation in production. The problem is compounded by labels that have unbounded or high-entropy values:&lt;/p&gt;



&lt;ul class=&quot;wp-block-list&quot;&gt;&lt;li&gt;User IDs or session tokens embedded in labels&lt;/li&gt;&lt;li&gt;Request IDs or trace IDs (effectively infinite cardinality)&lt;/li&gt;&lt;li&gt;Pod names without proper aggregation, especially in autoscaling environments&lt;/li&gt;&lt;li&gt;Free-form error messages or SQL query strings&lt;/li&gt;&lt;li&gt;IP addresses, particularly in environments with high churn&lt;/li&gt;&lt;/ul&gt;



&lt;p&gt;The relationship between cardinality and resource consumption is not linear &amp;mdash; it is roughly proportional but carries significant overhead per series in memory indexing structures. Prometheus stores its head block (the most recent data) entirely in memory. Each time series in the head block requires approximately 3&amp;ndash;4 KB of RAM for the series itself plus index entries. A Prometheus instance with 1 million active time series will typically consume 4&amp;ndash;6 GB of RAM just for the head block, before accounting for query processing overhead.&lt;/p&gt;



&lt;h2 class=&quot;wp-block-heading&quot;&gt;Memory Explosion Patterns and Real Symptoms&lt;/h2&gt;



&lt;p&gt;Memory issues in Prometheus rarely announce themselves cleanly. Instead, they manifest through a cascade of symptoms that are easy to misdiagnose. Understanding the failure modes helps you identify the root cause faster and apply the right remedy.&lt;/p&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;The Head Block Growth Pattern&lt;/h3&gt;



&lt;p&gt;Prometheus keeps a two-hour window of data in memory as the head block before compacting it to disk. If your series count grows continuously &amp;mdash; which happens when pod churn creates new series faster than old ones expire &amp;mdash; the head block never shrinks. You can monitor this directly with &lt;code&gt;prometheus_tsdb_head_series&lt;/code&gt; and &lt;code&gt;prometheus_tsdb_head_chunks&lt;/code&gt;. A healthy instance shows this number plateauing. A cardinality problem shows it growing monotonically until OOM.&lt;/p&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;Query Timeout Cascades&lt;/h3&gt;



&lt;p&gt;As series count grows, even well-written PromQL queries that worked fine at 100k series become unbearably slow at 1M. Grafana dashboards start timing out, alert evaluation lags behind schedule, and Alertmanager begins receiving delayed or duplicated firing alerts. The &lt;code&gt;prometheus_rule_evaluation_duration_seconds&lt;/code&gt; metric is a reliable early warning &amp;mdash; when p99 evaluation time for your recording rules exceeds your evaluation interval, you have a problem.&lt;/p&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;Scrape Failures Under Memory Pressure&lt;/h3&gt;



&lt;p&gt;When Prometheus is under heavy memory pressure, its Go garbage collector starts spending more time collecting, which introduces latency into the scrape loop. Scrapes begin timing out, causing gaps in your data. This creates a deceptive situation where you have gaps in metrics precisely when your system is under stress &amp;mdash; exactly when you need monitoring most. Watch &lt;code&gt;up&lt;/code&gt; metric drops and &lt;code&gt;prometheus_target_scrapes_exceeded_sample_limit_total&lt;/code&gt; for these patterns.&lt;/p&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;Compaction Pressure&lt;/h3&gt;



&lt;p&gt;High cardinality also stresses the TSDB compaction process. Prometheus compacts head block data into persistent blocks every two hours. With millions of series, compaction can take tens of seconds to minutes, during which write performance degrades. &lt;code&gt;prometheus_tsdb_compaction_duration_seconds&lt;/code&gt; rising above 30 seconds is a warning sign. Compaction failures leave orphaned blocks on disk, gradually consuming storage and potentially corrupting the TSDB if left unaddressed.&lt;/p&gt;



&lt;h2 class=&quot;wp-block-heading&quot;&gt;Short-Term Fixes: Tactical Remediation&lt;/h2&gt;



&lt;p&gt;When you are dealing with a Prometheus instance under active stress, you need immediate relief before you can implement architectural changes. These techniques can be applied quickly and provide meaningful headroom while longer-term solutions are planned.&lt;/p&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;Recording Rules: Pre-Computing Aggregations&lt;/h3&gt;



&lt;p&gt;Recording rules are the most underutilized tool in the Prometheus toolbox. They allow you to pre-compute expensive PromQL expressions and store the results as new time series. The key benefit for scalability is that you can aggregate away high-cardinality dimensions, dramatically reducing the number of series that dashboards and alerts need to query at runtime.&lt;/p&gt;



&lt;p&gt;Consider an example where you have per-pod HTTP request rates with labels for &lt;code&gt;pod&lt;/code&gt;, &lt;code&gt;namespace&lt;/code&gt;, &lt;code&gt;service&lt;/code&gt;, &lt;code&gt;method&lt;/code&gt;, and &lt;code&gt;status_code&lt;/code&gt;. Your dashboards mostly need service-level aggregations, not per-pod breakdowns. A recording rule can produce that aggregation once per evaluation interval:&lt;/p&gt;



&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;groups:
  - name: http_aggregations
    interval: 30s
    rules:
      - record: job:http_requests_total:rate5m
        expr: |
          sum by (job, namespace, method, status_code) (
            rate(http_requests_total[5m])
          )

      - record: job:http_request_duration_seconds:p99_5m
        expr: |
          histogram_quantile(0.99,
            sum by (job, namespace, le) (
              rate(http_request_duration_seconds_bucket[5m])
            )
          )

      - record: namespace:http_requests_total:rate5m
        expr: |
          sum by (namespace, status_code) (
            rate(http_requests_total[5m])
          )&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Notice that the &lt;code&gt;pod&lt;/code&gt; label is dropped in all three rules. If you had 500 pods, you have just reduced the cardinality of these series by a factor of 500. Dashboards querying &lt;code&gt;job:http_requests_total:rate5m&lt;/code&gt; instead of computing &lt;code&gt;rate(http_requests_total[5m])&lt;/code&gt; on the fly will return results orders of magnitude faster.&lt;/p&gt;



&lt;p&gt;The naming convention &lt;code&gt;level:metric:operations&lt;/code&gt; is the Prometheus community standard. Following it consistently makes recording rules self-documenting and helps teams understand the aggregation level at a glance.&lt;/p&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;Metric Dropping via Relabeling&lt;/h3&gt;



&lt;p&gt;Relabeling gives you surgical control over what metrics Prometheus actually ingests. There are two stages where relabeling applies: &lt;code&gt;relabel_configs&lt;/code&gt; (applied before scraping, based on target metadata) and &lt;code&gt;metric_relabel_configs&lt;/code&gt; (applied after scraping, based on scraped metric names and labels). For cardinality control, &lt;code&gt;metric_relabel_configs&lt;/code&gt; is your primary tool.&lt;/p&gt;



&lt;p&gt;Dropping entire metric families that you do not use is the most impactful change you can make. Many exporters emit dozens of metrics that are irrelevant for most use cases:&lt;/p&gt;



&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;scrape_configs:
  - job_name: kubernetes-pods
    metric_relabel_configs:
      # Drop metrics we never query
      - source_labels: [__name__]
        regex: 'go_gc_.*|go_memstats_.*|process_.*'
        action: drop

      # Drop high-cardinality label values while keeping the metric
      - source_labels: [__name__, pod]
        regex: 'http_requests_total;.*'
        target_label: pod
        replacement: ''

      # Drop entire time series based on label combinations
      - source_labels: [__name__, le]
        regex: 'http_request_duration_seconds_bucket;(\+Inf|100|250|500)'
        action: keep

      # Replace high-cardinality endpoint paths with normalized versions
      - source_labels: [endpoint]
        regex: '/api/v1/users/[0-9]+'
        target_label: endpoint
        replacement: '/api/v1/users/:id'&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Be careful with &lt;code&gt;metric_relabel_configs&lt;/code&gt; &amp;mdash; they are applied per scraped sample, so computationally expensive regex patterns across high-frequency scrapes can add CPU overhead. Test regex patterns and prefer anchored, non-backtracking expressions.&lt;/p&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;Cardinality Limits as a Safety Net&lt;/h3&gt;



&lt;p&gt;Prometheus 2.x introduced per-scrape sample limits as a defensive mechanism. These do not solve cardinality problems but prevent a single misbehaving exporter from taking down your entire Prometheus instance:&lt;/p&gt;



&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;global:
  # Global limit across all scrapes
  sample_limit: 0  # 0 = no limit

scrape_configs:
  - job_name: application-pods
    # Reject scrapes that return more than 50k samples
    sample_limit: 50000

    # Limit unique label sets per scrape
    label_limit: 64

    # Limit label name and value lengths
    label_name_length_limit: 256
    label_value_length_limit: 1024

    kubernetes_sd_configs:
      - role: pod&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;When a scrape exceeds &lt;code&gt;sample_limit&lt;/code&gt;, Prometheus rejects the entire scrape and marks the target as having failed. This is a hard circuit breaker, not a graceful degradation &amp;mdash; the target&amp;rsquo;s &lt;code&gt;up&lt;/code&gt; metric goes to 0. Set limits conservatively above your expected maximum to avoid false positives, and alert on &lt;code&gt;prometheus_target_scrapes_exceeded_sample_limit_total &amp;gt; 0&lt;/code&gt;.&lt;/p&gt;



&lt;h2 class=&quot;wp-block-heading&quot;&gt;Architectural Solutions: Federation and Remote Write&lt;/h2&gt;



&lt;p&gt;Once you have exhausted tactical optimizations or when your scale genuinely exceeds what a single Prometheus instance can handle, architectural changes become necessary. Prometheus offers two built-in mechanisms for scaling horizontally: federation and remote_write.&lt;/p&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;Federation: Hierarchical Scraping&lt;/h3&gt;



&lt;p&gt;Prometheus federation allows one Prometheus instance to scrape aggregated metrics from other Prometheus instances via the &lt;code&gt;/federate&lt;/code&gt; endpoint. In a typical setup, leaf-level Prometheus instances collect raw metrics from targets, while a global Prometheus instance federates pre-aggregated recording rule results from the leaves.&lt;/p&gt;



&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;# Global Prometheus configuration federating from regional instances
scrape_configs:
  - job_name: federate-eu-west
    scrape_interval: 15s
    honor_labels: true
    metrics_path: /federate
    params:
      match[]:
        # Only federate pre-aggregated recording rule metrics
        - '{__name__=~&quot;job:.*&quot;}'
        - '{__name__=~&quot;namespace:.*&quot;}'
        - '{__name__=~&quot;cluster:.*&quot;}'
        # Federate key infrastructure alerts
        - 'up{job=&quot;kubernetes-apiservers&quot;}'
    static_configs:
      - targets:
          - prometheus-eu-west.monitoring.svc:9090
          - prometheus-us-east.monitoring.svc:9090
          - prometheus-ap-south.monitoring.svc:9090&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Federation works well for multi-region global dashboards and cross-cluster alerting on aggregated signals. Its limitations are significant, though: the &lt;code&gt;/federate&lt;/code&gt; endpoint is a point-in-time snapshot, so you cannot run range queries against federated data effectively. It also creates a single point of failure at the global layer and does not provide true long-term storage. For those requirements, remote_write is the better path.&lt;/p&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;Remote Write: Streaming to Durable Storage&lt;/h3&gt;



&lt;p&gt;Remote write allows Prometheus to stream all ingested samples to an external storage backend in real time. The external backend handles long-term retention, multi-tenancy, and global query federation. Prometheus itself becomes a stateless collection agent that maintains only a short local retention window for resilience against network outages.&lt;/p&gt;



&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;remote_write:
  - url: &lt;a href=&quot;https://thanos-receive.monitoring.svc:19291/api/v1/receive&quot; rel=&quot;nofollow&quot;&gt;https://thanos-receive.monitoring.svc:19291/api/v1/receive&lt;/a&gt;
    # Authentication for the remote endpoint
    basic_auth:
      username: prometheus
      password_file: /etc/prometheus/secrets/remote-write-password

    # Tune the write queue for throughput vs. latency
    queue_config:
      # Number of shards (parallel write connections)
      max_shards: 200
      min_shards: 1
      # Samples to batch before flushing
      max_samples_per_send: 500
      # Time to wait before flushing an incomplete batch
      batch_send_deadline: 5s
      # In-memory buffer capacity per shard
      capacity: 2500
      # How long to retry failed writes
      min_backoff: 30ms
      max_backoff: 5s

    # Metadata configuration
    metadata_config:
      send: true
      send_interval: 1m

    # Filter what gets remote-written (reduce egress)
    write_relabel_configs:
      - source_labels: [__name__]
        regex: 'go_gc_.*|go_memstats_.*'
        action: drop&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;The &lt;code&gt;queue_config&lt;/code&gt; tuning is critical and frequently misunderstood. Each shard maintains its own connection to the remote endpoint and its own in-memory queue. Increasing &lt;code&gt;max_shards&lt;/code&gt; increases parallelism and throughput but also increases memory consumption and load on the remote endpoint. The right values depend heavily on your sample ingestion rate and network latency to the remote endpoint. Monitor &lt;code&gt;prometheus_remote_storage_queue_highest_sent_timestamp_seconds&lt;/code&gt; versus &lt;code&gt;prometheus_remote_storage_highest_timestamp_in_seconds&lt;/code&gt; &amp;mdash; the lag between them tells you how far behind your remote write queue is.&lt;/p&gt;



&lt;h2 class=&quot;wp-block-heading&quot;&gt;Long-Term Solutions: Thanos vs Grafana Mimir vs VictoriaMetrics&lt;/h2&gt;



&lt;p&gt;For production systems that need long-term storage, global query capability, high availability, and genuine horizontal scalability, purpose-built solutions are the right answer. Three projects dominate this space: Thanos, Grafana Mimir, and VictoriaMetrics. They share similar goals but differ significantly in architecture, operational complexity, and trade-offs.&lt;/p&gt;



&lt;figure class=&quot;wp-block-table&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Criterion&lt;/th&gt;&lt;th&gt;Thanos&lt;/th&gt;&lt;th&gt;Grafana Mimir&lt;/th&gt;&lt;th&gt;VictoriaMetrics&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Architecture&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Sidecar + object store; modular components&lt;/td&gt;&lt;td&gt;Fully distributed; Cortex-derived microservices&lt;/td&gt;&lt;td&gt;Single binary or cluster mode&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Storage backend&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Any S3-compatible object store&lt;/td&gt;&lt;td&gt;Any S3-compatible object store&lt;/td&gt;&lt;td&gt;Own TSDB format on local or object store&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;PromQL compatibility&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Full PromQL; own query engine&lt;/td&gt;&lt;td&gt;Full PromQL; Mimir-specific extensions&lt;/td&gt;&lt;td&gt;MetricsQL (PromQL superset)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Operational complexity&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Medium &amp;mdash; multiple components, each simple&lt;/td&gt;&lt;td&gt;High &amp;mdash; many microservices with complex config&lt;/td&gt;&lt;td&gt;Low &amp;mdash; minimal components, simple config&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Ingest scalability&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Scales via Thanos Receive fan-out&lt;/td&gt;&lt;td&gt;Horizontally scalable distributors + ingesters&lt;/td&gt;&lt;td&gt;Excellent; handles millions of samples/sec per node&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Query performance&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Good; Store Gateway caches object store data&lt;/td&gt;&lt;td&gt;Good; query sharding and caching built in&lt;/td&gt;&lt;td&gt;Excellent; highly optimized query engine&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Multi-tenancy&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Limited; tenant isolation via external labels&lt;/td&gt;&lt;td&gt;Native; per-tenant limits and isolation&lt;/td&gt;&lt;td&gt;Enterprise only; basic in cluster mode&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Deduplication&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Built-in; replica dedup at query time&lt;/td&gt;&lt;td&gt;Built-in; ingest-time and query-time dedup&lt;/td&gt;&lt;td&gt;Built-in; dedup with downsampling&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Downsampling&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Yes; Thanos Compactor handles it&lt;/td&gt;&lt;td&gt;Yes; configurable per tenant&lt;/td&gt;&lt;td&gt;Yes; automatic with vmbackupmanager&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;License&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Apache 2.0 (fully open source)&lt;/td&gt;&lt;td&gt;AGPL-3.0 (open source) + enterprise tier&lt;/td&gt;&lt;td&gt;Apache 2.0 (community); proprietary enterprise&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Best fit&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Teams already running Prometheus wanting minimal disruption&lt;/td&gt;&lt;td&gt;Large orgs needing multi-tenant SaaS-grade monitoring&lt;/td&gt;&lt;td&gt;Teams prioritizing simplicity and raw performance&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/figure&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;Thanos: The Incremental Path&lt;/h3&gt;



&lt;p&gt;Thanos integrates with existing Prometheus deployments through a sidecar process that runs alongside each Prometheus pod. The sidecar uploads completed TSDB blocks to object storage (S3, GCS, Azure Blob) and exposes a gRPC Store API that Thanos Query uses to federate queries across all Prometheus instances plus historical data in the object store. This makes Thanos the lowest-friction path for teams with existing Prometheus infrastructure.&lt;/p&gt;



&lt;p&gt;Thanos Receive is an alternative ingest path that accepts remote_write directly, which is useful when you want to decouple Prometheus instances from the query layer or implement active-active HA without relying on Prometheus replication. Thanos Compactor handles block compaction and downsampling on the object store, creating 5-minute and 1-hour resolution downsamples automatically for efficient long-range queries.&lt;/p&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;Grafana Mimir: Enterprise-Grade Multi-Tenancy&lt;/h3&gt;



&lt;p&gt;Mimir is a fork of Cortex, rewritten by Grafana Labs to address operational complexity issues in Cortex&amp;rsquo;s architecture. It follows the same microservices pattern &amp;mdash; Distributor, Ingester, Querier, Query Frontend, Store Gateway, Compactor, Ruler &amp;mdash; but with significantly improved defaults and a monolithic deployment mode that simplifies small-scale deployments. Mimir&amp;rsquo;s headline feature is native multi-tenan&lt;/p&gt;
</description>
<category>Other</category>
<guid isPermaLink="true">https://coderlegion.com/15639/prometheus-scalability-high-cardinality-and-how-to-fix-it</guid>
<pubDate>Sat, 25 Apr 2026 02:25:50 +0000</pubDate>
</item>
</channel>
</rss>