How to Design Prometheus — Cloud-Native Monitoring System
A Senior+ Guide to Building Production-Grade Observability with Prometheus, PromQL, TSDB & Alertmanager
1. Introduction: Prometheus at Scale
Prometheus has become the de facto standard for cloud-native monitoring since its creation at SoundCloud in 2012 and its subsequent graduation from the Cloud Native Computing Foundation (CNCF) in 2018 — the second project to graduate after Kubernetes itself. In a landscape dominated by proprietary monitoring solutions, Prometheus carved its niche by embracing an open-source, pull-based model that aligns perfectly with the ephemeral, containerized workloads of modern infrastructure. Today, organizations ranging from startup greenfield projects to Fortune 500 enterprises rely on Prometheus as their primary metrics collection and alerting backbone, and it consistently ranks among the most actively maintained CNCF projects by contributor count and commit velocity.
The fundamental design philosophy behind Prometheus centers on a pull-based metrics collection model. Unlike traditional push-based systems where applications are responsible for shipping their telemetry data to a central collector, Prometheus actively scrapes HTTP endpoints exposed by target services at configurable intervals. This pull model provides several distinctive advantages: it eliminates the need for complex instrumentation libraries that must push data outbound, it allows Prometheus to discover and monitor targets dynamically without any configuration changes on the application side, and it provides a clear backpressure mechanism — if Prometheus is temporarily unavailable, the targets continue running without interruption and no data is permanently lost since the metrics remain available for the next scrape cycle.
Under the hood, Prometheus employs a purpose-built time series database (TSDB) that is optimized for write-heavy workloads and high-cardinality label combinations. The TSDB uses a local storage model based on append-only files organized into two-hour blocks, which enables efficient compression and fast query performance. The storage layer achieves remarkable compression ratios — often 10-15x compression on raw metric data — by leveraging techniques like delta-of-delta encoding for timestamps and XOR-based floating-point compression for sample values. Understanding these internal mechanisms is critical for senior engineers who need to tune retention policies, plan capacity, and troubleshoot performance issues at scale.
In production environments, a single Prometheus server can comfortably handle tens of millions of active time series with scrape intervals of 15-30 seconds. However, as organizations grow and their monitoring requirements expand across multiple clusters, regions, and teams, the single-server architecture reaches its limits. This is where the broader Prometheus ecosystem — including Thanos, Mimir, Cortex, and VictoriaMetrics — comes into play. These projects extend Prometheus with long-term storage, global query views, and horizontal scalability while preserving the core PromQL query language and pull-based collection model that made Prometheus successful in the first place.
This guide is designed for senior engineers and architects who need to design, deploy, and operate Prometheus at production scale. We will dissect every major component — from the TSDB internals and PromQL query optimization to federation topologies, remote write architectures, and security hardening — with practical code examples and architectural diagrams. By the end, you will have a comprehensive understanding of how to build a monitoring system that can scale from a single Kubernetes cluster to a multi-region, multi-cloud observability platform serving hundreds of teams and billions of data points per day.
The monitoring landscape in 2026 has converged on OpenTelemetry as the vendor-neutral standard for telemetry collection, and Prometheus integrates seamlessly with this ecosystem. The Prometheus receiver in the OpenTelemetry Collector can accept OTLP metrics and forward them to Prometheus-compatible backends, while the Prometheus remote write exporter allows Prometheus to forward its collected metrics to OpenTelemetry-aware backends. This convergence means that investing in Prometheus expertise remains highly valuable even as the telemetry collection layer evolves, since the query, storage, and alerting capabilities of Prometheus and its ecosystem continue to be the industry benchmark against which all monitoring systems are measured.
Whether you are building a greenfield monitoring platform, migrating from a legacy monitoring solution like Nagios or Zabbix, or scaling an existing Prometheus deployment to handle increased load, this guide provides the architectural patterns, configuration recipes, and operational insights you need to succeed. We will focus heavily on real-world production considerations rather than toy examples, because the difference between a monitoring system that works in a demo and one that reliably operates at scale is precisely the kind of depth that separates senior engineers from their peers. The evolution from a single Prometheus instance to a globally distributed monitoring platform is a journey that requires careful planning, and this guide serves as your comprehensive roadmap for that transformation.
2. Core Architecture
The Prometheus architecture consists of four primary components working together in a carefully orchestrated pipeline: the Prometheus Server (which handles both scraping and storage), Exporters (which expose metrics from third-party systems in Prometheus format), the Pushgateway (for ephemeral and batch job metrics), and Alertmanager (which handles deduplication, routing, and notification of alerts). Understanding how these components interact — and critically, where the failure boundaries lie — is essential for designing a resilient monitoring system that can survive component failures without leaving blind spots in your observability coverage.
2.1 Prometheus Server Internals
The Prometheus Server is the heart of the system and comprises three tightly integrated subsystems: the retrieval engine (scraper), the TSDB storage engine, and the HTTP API server. The retrieval engine is responsible for discovering targets, scheduling scrape loops, and collecting metric samples over HTTP. Each scrape produces a set of time series samples that are written into the TSDB's in-memory head block, which is periodically flushed to durable on-disk blocks. The HTTP API server exposes both a query endpoint compatible with PromQL and a series endpoint for programmatic access to raw metric data.
Internally, Prometheus maintains a scrape loop for each configured target. Each scrape loop operates independently with its own HTTP client, timeout configuration, and retry logic. The default scrape timeout is 10 seconds, but this should be tuned based on the expected response time of the targets being scraped. For targets exposing millions of metrics, the scrape can take significantly longer, and you may need to increase the timeout to 30-60 seconds while carefully balancing against the scrape interval to avoid overlapping scrapes.
2.2 Exporters
Exporters are lightweight adapter processes that translate metrics from third-party systems into the Prometheus exposition format. The most commonly deployed exporter is the Node Exporter, which exposes hardware and OS-level metrics from Linux and Unix systems. Other popular exporters include the MySQL Exporter, PostgreSQL Exporter, Redis Exporter, Elasticsearch Exporter, and the Blackbox Exporter for probing external endpoints. Each exporter typically runs as a sidecar or DaemonSet in Kubernetes environments, exposing a metrics HTTP endpoint on a dedicated port.
2.3 Pushgateway
The Pushgateway serves as an intermediary for metrics that cannot be pulled by Prometheus — specifically, short-lived batch jobs and cron jobs that complete before Prometheus can scrape them. These jobs push their metrics to the Pushgateway, which holds them in memory until Prometheus scrapes the Pushgateway itself. This is the one exception to Prometheus's pull-based model and should be used sparingly because the Pushgateway introduces a single point of failure and can become a bottleneck if misused for long-running services that should be scraped directly.
2.4 Alertmanager
Alertmanager is a separate process that receives alert notifications from Prometheus and handles deduplication, grouping, routing, and notification delivery. When Prometheus fires an alert, it sends it to Alertmanager via HTTP. Alertmanager then applies its routing tree — which can route alerts to different receivers based on label matchers — groups related alerts together to avoid notification storms, applies inhibition rules to suppress lower-priority alerts when higher-priority ones are active, and manages silences for planned maintenance windows. This separation of concerns ensures that the monitoring and alerting responsibilities are cleanly decoupled.
2.5 Component Communication Protocol
All communication between Prometheus components happens over HTTP/1.1 or HTTP/2, with protobuf encoding used for high-throughput communication channels like remote write. The Prometheus server pulls metrics from targets using a simple GET request to the /metrics endpoint. Alertmanager notifications are delivered via HTTP POST to webhook endpoints or via SMTP for email notifications. Understanding this protocol-level detail is important for debugging connectivity issues in production and for designing custom integrations that must operate within the Prometheus ecosystem reliably and efficiently.
| Component | Default Port | Protocol | Data Flow | Failure Impact |
|---|---|---|---|---|
| Prometheus Server | 9090 | HTTP | Pull from targets | No metrics collection or querying |
| Node Exporter | 9100 | HTTP | Exposed to Prometheus | Host-level metrics gap |
| Pushgateway | 9091 | HTTP | Push from jobs, pull by Prometheus | Batch job metrics lost |
| Alertmanager | 9093 | HTTP | Receives from Prometheus | No alert notifications delivered |
| cAdvisor | 8080 | HTTP | Container metrics | No container-level metrics |
| Kube-State-Metrics | 8080 | HTTP | K8s object metrics | No Kubernetes object metrics |
3. Data Model — Metrics Types
Prometheus's data model is built around the concept of a time series identified by a metric name and a set of key-value label pairs. Each individual time series is uniquely identified by its metric name plus its label set — for example, http_requests_total{method="POST", handler="/api/v1/users", status="200"} is a distinct time series. The label-based data model provides extraordinary flexibility for slicing and dicing metrics along multiple dimensions, which is why Prometheus excels in environments where services are highly dynamic and multi-tenant.
Prometheus defines four core metric types, each designed for a specific class of measurement. Choosing the correct type is critical because it determines how the metric can be queried, aggregated, and interpreted. Misusing metric types is one of the most common mistakes in Prometheus instrumentation — for instance, using a Gauge when a Counter is appropriate will make rate calculations meaningless, while using a Summary instead of a Histogram prevents server-side aggregation of quantile calculations. The distinction between these types has profound implications for the mathematical correctness of your queries and the operational value of your dashboards.
3.1 Counter
A Counter is a monotonically increasing value that can only go up or be reset to zero on process restart. Counters are used to measure things that accumulate over time — total requests received, total bytes transferred, total errors encountered. The primary use case for Counters is computing rate of change using the rate() or irate() functions. Counter resets (due to process restarts) are handled gracefully by the rate() function, which detects the reset and adjusts the calculation accordingly, making it safe to use across restarts without losing accuracy in your rate computations.
C#
// C# Counter instrumentation example using Prometheus.NET
using Prometheus;
public class OrderMetrics
{
// Counter: monotonically increasing, tracks total orders processed
private static readonly Counter TotalOrders = Prometheus.Metrics
.CreateCounter("orders_total", "Total number of orders processed",
new[] { "region", "payment_method" });
// Counter with custom labels for fine-grained tracking
private static readonly Counter OrderRevenue = Prometheus.Metrics
.CreateCounter("order_revenue_dollars_total", "Total revenue from orders",
new[] { "currency", "region" });
public void ProcessOrder(Order order)
{
// Increment counter with label values
TotalOrders.WithLabels(order.Region, order.PaymentMethod).Inc();
OrderRevenue.WithLabels(order.Currency, order.Region)
.Inc((double)order.Amount);
}
}
// Exposing metrics endpoint in ASP.NET Core
// In Program.cs or Startup.cs:
// app.MapMetrics(); // exposes /metrics endpoint
// Prometheus.Metrics.EnableCurrentMetrics();
3.2 Gauge
A Gauge is a numeric value that can go up and down arbitrarily — think temperature, memory usage, queue depth, number of active connections. Unlike Counters, Gauges represent a point-in-time snapshot of a value and are not meant to be used with rate calculations. Common operations on Gauges include avg_over_time(), min_over_time(), max_over_time(), and direct comparison with threshold values in alerting rules. Gauges are the most straightforward metric type but are often misused in place of Counters, leading to incorrect rate calculations and misleading dashboards.
C#
// C# Gauge instrumentation for runtime metrics
using Prometheus;
public class RuntimeMetricsCollector
{
private static readonly Gauge ActiveConnections = Prometheus.Metrics
.CreateGauge("app_active_connections",
"Current number of active connections",
new[] { "pool" });
private static readonly Gauge QueueDepth = Prometheus.Metrics
.CreateGauge("app_queue_depth",
"Current depth of the processing queue");
private static readonly Gauge CacheHitRatio = Prometheus.Metrics
.CreateGauge("app_cache_hit_ratio",
"Cache hit ratio as a percentage");
public void UpdateConnectionCount(string pool, int count)
{
// Set gauge to current value
ActiveConnections.WithLabels(pool).Set(count);
}
public void UpdateQueueDepth(int depth)
{
QueueDepth.Set(depth);
}
public void RecordCacheHitRatio(double ratio)
{
CacheHitRatio.Set(ratio);
}
}
3.3 Histogram
A Histogram samples observations and counts them in configurable buckets, while also maintaining a sum and count. Histograms are the recommended choice when you need to calculate quantiles (like p95, p99 latency) or when you need to aggregate distributions across multiple instances. The key insight is that Histogram quantile calculations happen at query time on the server side, which means you can compute the p99 latency across 100 instances by simply summing the individual instance histograms — something impossible with client-side Summary quantiles. The default bucket boundaries are tailored for HTTP request durations, but custom buckets should be configured based on the expected value distribution of your specific use case.
3.4 Summary
A Summary is similar to a Histogram but calculates quantiles on the client side during instrumentation. While this provides exact quantile values for a single instance, it prevents server-side aggregation — you cannot meaningfully average p99 latencies from two Summary measurements. Summaries are useful when you need exact quantiles without server-side computation and when the set of quantile buckets is known in advance. In most production scenarios, Histograms are preferred over Summaries for their flexibility and aggregation capabilities, though Summaries may be acceptable for simple single-instance monitoring where cross-instance aggregation is not required.
| Type | Use Case | Aggregation | Storage Cost | Example |
|---|---|---|---|---|
| Counter | Monotonically increasing values | rate(), sum() | Low | requests_total, bytes_sent_total |
| Gauge | Point-in-time values that fluctuate | avg(), min(), max() | Low | temperature_celsius, queue_depth |
| Histogram | Latency distributions, response sizes | histogram_quantile(), sum() | Medium-High (N buckets) | http_request_duration_seconds |
| Summary | Pre-computed quantiles per instance | Limited — no cross-instance | Medium (fixed quantiles) | go_gc_duration_seconds |
3.5 Label Design Best Practices
Label design is arguably the most critical decision in your Prometheus instrumentation strategy. Poor label choices lead to high-cardinality explosions that can crash your Prometheus server, while overly restrictive labels prevent useful analysis. The golden rule is: never use user IDs, request IDs, UUIDs, or other unbounded values as labels. Instead, use labels for dimensions that have a bounded set of values — like status codes, regions, endpoint groups (not individual endpoints), and method types. A good heuristic is that no single label should have more than a few hundred distinct values, and the product of all label cardinalities for a single metric should stay well under one million to prevent memory exhaustion in the TSDB head block.
When naming metrics, follow the convention of _total suffix for Counters, no suffix for Gauges, and _seconds or _bytes suffix for Histograms and Summaries to indicate the unit. Always use base units (seconds rather than milliseconds, bytes rather than kilobytes) to enable consistent cross-service aggregation. The metric name itself should describe what is being measured — prefer http_request_duration_seconds over http_latency because the former is self-documenting and immediately communicates both what and how it is measured, reducing the cognitive load on engineers who consume these metrics through dashboards and alerts.
4. PromQL Query Language
PromQL (Prometheus Query Language) is a powerful, functional query language designed specifically for selecting and aggregating time series data. Unlike SQL, which operates on tabular data with explicit schemas, PromQL operates on multi-dimensional time series and provides built-in functions for rate calculations, aggregation over time windows, quantile estimation, and anomaly detection. Mastering PromQL is the single most important skill for getting value out of a Prometheus deployment, and many of the concepts that seem confusing at first — like the difference between rate() and irate(), or when to use sum by() versus sum without() — become second nature with practice and deliberate study of the official documentation examples.
4.1 Basic Selectors
The simplest PromQL expression is a metric name, which returns all time series with that name. Label selectors narrow the result set using exact match (=), regex match (=~), not-equal match (!=), and not-regex match (!~). Multiple label matchers within the same curly braces are combined with AND logic. You can combine metric names with label matchers, arithmetic operators, and grouping clauses to build sophisticated queries that slice your metrics along any dimension your labels provide.
PromQL
# Select all HTTP request duration time series
http_request_duration_seconds_bucket
# Filter by specific labels
http_request_duration_seconds_bucket{job="api-server", method="POST"}
# Regex match: select all 2xx status codes
http_requests_total{status=~"2.."}
# Negation: exclude health check endpoints
http_requests_total{handler!~"/health|/ready"}
# Rate calculation: requests per second over 5-minute window
rate(http_requests_total[5m])
# Histogram quantile: 99th percentile latency across all instances
histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[5m])) by (le))
# Aggregation: total error rate grouped by service
sum(rate(http_requests_total{status=~"5.."}[5m])) by (job)
# Comparison: services with error rate above 1%
sum(rate(http_requests_total{status=~"5.."}[5m])) by (job)
/ sum(rate(http_requests_total[5m])) by (job) > 0.01
# Top-K: the 5 services with the highest request rates
topk(5, sum(rate(http_requests_total[5m])) by (job))
# Predict linear: predict disk usage in 4 hours based on 2-hour trend
predict_linear(node_filesystem_avail_bytes[2h], 4*3600)
# Absent: alert when a metric completely disappears
absent(up{job="critical-service"})
4.2 Aggregation Operators
PromQL provides a rich set of aggregation operators that can be applied across label dimensions. The sum, avg, min, max, count, stddev, stdvar, count_values, bottomk, and topk operators each perform a different type of aggregation. The by and without modifiers control which labels are preserved during aggregation. Using by (label1, label2) groups the result by those labels and sums all other dimensions, while without (label1) removes those labels and groups everything else together. This distinction is crucial for writing correct aggregation queries that produce the expected number of output time series.
4.3 Key Functions
PromQL's function library is categorized into several groups. Rate and increase functions (rate(), irate(), increase()) are used to compute per-second rates of change for Counters. The rate() function uses a least-squares regression over the entire time window to compute a smoothed rate, while irate() uses only the last two samples for an instantaneous rate that is more responsive to spikes but also more noisy. Time functions (time(), timestamp()) provide access to the current time or the last sample timestamp. Prediction functions (predict_linear()) use linear regression to forecast future values, which is invaluable for capacity planning alerts like predicting when a disk will be full based on the current write rate trend.
4.4 Recording Rules
Recording rules pre-compute frequently used or expensive PromQL expressions and save the result as a new time series. This is essential for dashboard performance — instead of computing histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[5m])) by (le, job)) every time a Grafana dashboard loads, a recording rule computes it every 30 seconds and stores the result, making dashboard queries nearly instantaneous. Recording rules also enable you to create higher-level service-level indicator (SLI) metrics that abstract away implementation details and provide a consistent interface for SLO tracking across teams and services, regardless of the underlying implementation specifics.
| Function | Category | Description | Typical Use |
|---|---|---|---|
rate() | Counter | Per-second rate of increase over time window | Requests per second, error rate |
irate() | Counter | Instant rate based on last two samples | Spiky metrics, fine-grained rate |
increase() | Counter | Total increase over time window | Hourly request count |
histogram_quantile() | Histogram | Quantile estimation from bucket counts | p50, p95, p99 latency |
predict_linear() | Forecast | Linear regression prediction | Disk fill time prediction |
absent() | Utility | Returns 1 if metric is missing | Deadman alerts |
label_replace() | Label | Regex-based label value transformation | Enriching labels at query time |
topk() | Aggregation | Returns top K series by value | Top consumers, busiest endpoints |
5. TSDB Storage Engine
The Prometheus TSDB (Time Series Database) is a purpose-built storage engine optimized for write-heavy, append-only workloads with high compression ratios and fast range queries. Understanding the TSDB internals is essential for capacity planning, retention tuning, and troubleshooting performance issues that arise in production environments at scale. The TSDB organizes data into a write-ahead log (WAL) for durability, an in-memory head block for recent data, and persistent on-disk blocks for historical data. This hierarchical storage model provides both fast writes (appending to the head block) and efficient queries (reading compressed persistent blocks), creating a balanced architecture that serves both operational dashboards and retrospective analysis.
5.1 Write-Ahead Log (WAL)
The WAL provides crash recovery by recording all incoming samples before they are committed to the head block. If the Prometheus process crashes, the TSDB can replay the WAL to recover any samples that were received but not yet flushed. The WAL uses memory-mapped files for performance and automatically truncates entries once they have been successfully flushed to the head block. The WAL segment size defaults to 32MB, and WAL compression can be enabled via the --storage.tsdb.wal-compression flag, which reduces WAL disk usage by approximately 50% without significant CPU overhead. In environments with high write throughput, the WAL can consume substantial disk I/O, so placing it on a dedicated SSD volume is recommended for production deployments handling millions of active time series.
5.2 Head Block
The head block is the in-memory active block that receives all new samples. It maintains a reference set of all active time series (the "head") and their most recent sample values. The head block flushes to disk every 2 hours by default, creating a new persistent block. During the flush, the head block creates a snapshot of its current state, writes all series data to disk chunks, builds the index, and then replaces the old head block with a fresh one. This 2-hour window means that the most recent 2 hours of data are always in memory, which directly impacts the memory requirements of your Prometheus server — plan for approximately 2-4KB per active time series in the head block, which means a server with 10 million active time series needs roughly 20-40GB of RAM just for the head block reference data and active chunks.
5.3 Persistent Blocks
Each persistent block contains four files: an index file, a chunks directory with the actual time series data, a meta.json file with block metadata, and a tombstones file recording deleted series. The index file uses a custom binary format with a symbol table for efficient string storage and posting lists for fast label-based lookups. Chunks are compressed using XOR-based floating-point compression (Gorilla encoding), which achieves excellent compression ratios for the types of time series data Prometheus typically stores — slowly changing gauges and monotonically increasing counters. Each chunk holds up to 2 hours of data for a single time series and is approximately 8KB in size after compression, regardless of the number of samples it contains.
5.4 Compaction
Compaction is the process of merging smaller blocks into larger ones to reduce the total number of blocks and improve query performance. The TSDB automatically compacts blocks at multiple levels — first merging adjacent 2-hour blocks into 8-hour blocks, then into 2-day blocks, and so on. Each compaction level reduces the number of index lookups required for range queries. The compaction process also applies tombstones (deletions) and can split or merge blocks based on time boundaries. The --storage.tsdb.min-block-duration and --storage.tsdb.max-block-duration flags control compaction behavior, and the prometheus_tsdb_compactions_total and prometheus_tsdb_compaction_duration_seconds metrics provide visibility into compaction health and performance, which is critical for capacity planning and identifying I/O bottlenecks before they impact query performance.
5.5 Memory Estimation
Estimating memory usage for a Prometheus deployment requires understanding the cost per active time series across several categories: the head block allocates approximately 2-4KB per series for the reference set and active chunks, the WAL requires memory for the in-progress segment (approximately 32MB per segment), and query execution allocates temporary memory proportional to the number of series being scanned. A useful rule of thumb is to allocate 4KB per active time series for the head block, plus an additional 2KB per series for query execution overhead. For a server with 10 million active time series, this translates to approximately 60GB of RAM just for the TSDB, plus additional memory for the Prometheus process itself, any recording or alerting rules, and the scrape buffers that hold in-flight HTTP responses from targets.
| TSDB Component | Purpose | Retention | Memory per Series | Key Configuration |
|---|---|---|---|---|
| WAL | Crash recovery | Until flushed to head | N/A | --storage.tsdb.wal-compression |
| Head Block | Recent data writes | 2 hours (default) | 2-4 KB | --storage.tsdb.min-block-duration |
| Persistent Blocks | Historical data | Retention policy | Minimal (memory-mapped) | --storage.tsdb.retention.time |
| Index | Series lookup | Per block lifetime | N/A | Auto-managed by compactor |
| Chunks | Raw time series data | Per block lifetime | N/A | 8KB per chunk file |
| Tombstones | Deletion markers | Until compaction | N/A | Applied during compaction |
6. Scraping Architecture
The scraping architecture is where Prometheus's pull-based model becomes tangible and operational. The Prometheus server maintains a set of scrape configurations that define which targets to monitor, how frequently to scrape them, what HTTP headers to send, and how to handle authentication and TLS. Each scrape configuration produces a set of scrape loops — one per target — that independently fetch metrics over HTTP. The scraping subsystem is tightly integrated with service discovery, which dynamically updates the set of targets based on changes in the underlying infrastructure, ensuring that new instances are automatically monitored and terminated instances are gracefully removed from the target pool.
6.1 Scrape Configuration Structure
A scrape configuration specifies the job name, scrape interval, scrape timeout, static targets or service discovery mechanism, HTTP client settings (TLS, auth, proxies), and relabeling rules. The scrape interval determines how often Prometheus polls each target — common values are 15s, 30s, or 60s. The scrape timeout must be shorter than the scrape interval to ensure that a slow scrape does not overlap with the next one. For most workloads, a 15-second scrape interval provides a good balance between granularity and resource consumption, while a 30-second interval is more appropriate for less time-sensitive metrics or resource-constrained environments where the overhead of frequent scraping would impact target performance.
YAML
# prometheus.yml - Production scrape configuration
global:
scrape_interval: 15s
scrape_timeout: 10s
evaluation_interval: 15s
scrape_configs:
- job_name: 'kubernetes-pods'
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_namespace]
target_label: namespace
- source_labels: [__meta_kubernetes_pod_name]
target_label: pod
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_port]
target_label: __address__
regex: (.+)
replacement: ${1}
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: true
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
target_label: __metrics_path__
regex: (.+)
replacement: ${1}
- job_name: 'node-exporters'
static_configs:
- targets:
- 'node1.internal:9100'
- 'node2.internal:9100'
- 'node3.internal:9100'
metric_relabel_configs:
- source_labels: [__name__]
action: drop
regex: 'node_netstat_.*'
- job_name: 'mysql'
static_configs:
- targets: ['mysql-exporter:9104']
scrape_interval: 30s
scrape_timeout: 20s
6.2 Relabeling
Relabeling is one of Prometheus's most powerful features and is essential for controlling which targets are scraped and what labels are attached to scraped metrics. There are two types of relabeling: target relabeling (which applies to the target itself before scraping) and metric relabeling (which applies to individual metric samples after scraping). Both use the same mechanism — a set of source labels, a separator, a regex, a target label, and a replacement string. The regex is applied to the concatenated source labels, and the replacement is written to the target label. If the regex does not match, the default replacement is used. Target relabeling is commonly used to filter targets (with the keep and drop actions), modify the scrape address, set the metrics path, or add custom labels. Metric relabeling is used to drop high-cardinality or irrelevant metrics before they are written to the TSDB, rename metrics, or add computed labels based on existing label values.
6.3 Scrape Performance Considerations
The scrape subsystem's performance is governed by several factors: the number of targets, the scrape interval, the number of metrics per target, and the network latency between Prometheus and targets. Each scrape consumes approximately one TCP connection (HTTP keepalive is enabled by default), and the Prometheus server maintains a connection pool with a configurable maximum. For large deployments, it is common to shard targets across multiple Prometheus servers using hashmod-based target splitting, where each Prometheus instance is responsible for a subset of targets based on a hash of the target address modulo the number of instances. This horizontal scaling approach is necessary when the aggregate scrape load exceeds what a single Prometheus server can handle, which typically occurs at around 500-1000 targets per server depending on the metrics per target and the desired scrape interval.
| Configuration | Default | Recommended (Small) | Recommended (Large) | Impact |
|---|---|---|---|---|
| scrape_interval | 1m | 15s | 30s | Granularity vs resource usage |
| scrape_timeout | 10s | 10s | 20s | Must be less than scrape_interval |
| sample_limit | 0 (unlimited) | 5000 | 10000 | Max samples per scrape |
| target_limit | 0 (unlimited) | 1000 | 5000 | Max targets per job |
| max_scrape_size | 0 (unlimited) | 50MB | 100MB | Max response body size |
7. Service Discovery
Service discovery is the mechanism by which Prometheus dynamically discovers monitoring targets without requiring manual configuration updates. This is fundamental to Prometheus's effectiveness in cloud-native environments where containers are created and destroyed continuously, IP addresses change, and services scale up and down automatically. Prometheus supports over a dozen service discovery mechanisms out of the box, and the choice of mechanism depends on your infrastructure platform and deployment model. The service discovery subsystem is one of the most sophisticated parts of Prometheus, involving target groups, label injection, and real-time event processing that keeps the set of monitored targets in sync with the actual infrastructure state.
7.1 Kubernetes Service Discovery
Kubernetes service discovery is the most commonly used mechanism in cloud-native environments. Prometheus can discover targets by watching the Kubernetes API server for changes to Pods, Services, Endpoints, Nodes, and Ingress objects. The most flexible approach is Pod-based discovery, which discovers every container port in every Pod that matches the configured role. Combined with annotations on Pods (like prometheus.io/scrape: "true" and prometheus.io/port: "9090"), this provides a fully declarative approach to monitoring where teams simply annotate their deployments and Prometheus automatically begins scraping them without any intervention from the platform team. This annotation-driven model has become the industry standard for Prometheus-based monitoring in Kubernetes.
7.2 Consul Service Discovery
For non-Kubernetes environments, Consul service discovery is a popular choice. Prometheus watches Consul for registered services and their health status, automatically adding healthy instances to the scrape target list and removing unhealthy ones. This integration is particularly valuable for hybrid environments where some workloads run on VMs managed by Consul while others run on Kubernetes. The Consul SD configuration supports filtering by datacenter, service tags, and health status, providing fine-grained control over which services are monitored and which instances within a service are actively scraped.
7.3 DNS Service Discovery
DNS-based service discovery uses DNS A, AAAA, or SRV records to discover targets. This is the simplest discovery mechanism and works with any infrastructure that exposes DNS records — including Consul DNS, Kubernetes CoreDNS, and cloud provider DNS services. DNS SD is particularly useful for discovering targets behind load balancers or in environments where DNS is the primary service registry. However, DNS SD does not provide metadata like service tags or health status, making it less feature-rich than Consul or Kubernetes SD, and the discovery latency depends on the DNS TTL configuration which can range from seconds to minutes depending on the infrastructure.
7.4 EC2 and Cloud Provider Discovery
Cloud provider service discovery enables Prometheus to discover targets based on cloud-specific metadata — EC2 instance tags, GCP instance labels, Azure VM tags, or OpenStack server metadata. This is invaluable for monitoring infrastructure that is not running in Kubernetes, where each cloud instance can be annotated with tags that Prometheus translates into labels. For example, you can tag EC2 instances with Environment=production and Team=platform, and Prometheus will automatically add these as labels to all metrics scraped from those instances, enabling environment-aware dashboards and team-based alerting without any application-level changes.
| Discovery Mechanism | Best For | Dynamic? | Metadata Richness | Setup Complexity |
|---|---|---|---|---|
| Kubernetes (Pod) | K8s workloads | Yes - real-time | High (annotations, labels) | Low |
| Kubernetes (Service) | Service-level monitoring | Yes - real-time | Medium | Low |
| Consul | VM/hybrid environments | Yes - near real-time | High (tags, health) | Medium |
| DNS (A/SRV) | Simple environments | Depends on TTL | Low (IP only) | Low |
| EC2 | AWS infrastructure | Periodic polling | Medium (tags, AZ) | Medium |
| Azure | Azure infrastructure | Periodic polling | Medium (tags, RG) | Medium |
| GCP | GCP infrastructure | Periodic polling | Medium (labels, zone) | Medium |
| Static | Fixed infrastructure | No | Manual labels only | None |
8. Alertmanager
Alertmanager is the dedicated alert processing and notification component of the Prometheus ecosystem. While Prometheus is responsible for evaluating alerting rules and firing alerts, Alertmanager handles the operational complexity of managing those alerts — deduplicating identical alerts from multiple Prometheus instances, grouping related alerts into a single notification, applying inhibition rules to suppress lower-priority alerts when critical ones are active, routing alerts to different receivers based on label-based matchers, and managing silence windows for planned maintenance. This separation of concerns is essential for production reliability because it prevents duplicate notifications from overwhelming on-call engineers and ensures that alerts reach the right teams through the right channels at the right time.
8.1 Alert Routing
Alertmanager's routing tree is configured as a hierarchy of routes, where each route specifies label matchers and a receiver. When an alert arrives, it is matched against the top-level routes in order. The first matching route is applied, and if it has child routes, those are checked recursively. This hierarchical model allows for sophisticated routing logic — for example, routing all alerts to a default team channel, but overriding for alerts with severity=critical to go directly to PagerDuty, while routing team=database alerts to a dedicated database on-call channel. Each route can also specify group_by labels, group_wait (how long to wait before sending the first notification for a group), group_interval (how long to wait between notifications for the same group), and repeat_interval (how often to resend a notification for an ongoing alert that has not been resolved).
8.2 Inhibition Rules
Inhibition rules suppress notifications for lower-priority alerts when higher-priority alerts are active. For example, if a node is down (node_down alert), all service-level alerts on that node (high_latency, error_rate, etc.) should be inhibited because they are caused by the node failure, not independent issues. Inhibition is configured by specifying source matchers and target matchers — if any alert matching the source matchers is firing, alerts matching the target matchers are suppressed. This dramatically reduces alert noise during infrastructure failures and ensures that on-call engineers focus on the root cause rather than drowning in cascading symptoms that all stem from the same underlying problem.
8.3 Silences
Silences are temporary suppressions of alerts, typically used during planned maintenance windows or known outage periods. A silence is defined by a set of label matchers and a start/end time. Any alert that matches all the label matchers is silenced for the duration of the silence. Silences are managed via the Alertmanager web UI or API, and they persist across Alertmanager restarts. It is important to note that silences do not prevent Prometheus from firing the alert — they only prevent Alertmanager from sending the notification. This distinction is important for post-incident analysis because silenced alerts still appear in the Alertmanager UI as "silenced" rather than being invisible, ensuring auditability and preventing accidentally silenced critical alerts from going unnoticed.
8.4 Clustering
Alertmanager supports native clustering using the Gossip protocol. When multiple Alertmanager instances are configured as a cluster, they share information about received alerts, silences, and inhibition state. This means that all instances have the same view of the alert landscape, and notifications are deduplicated across the cluster — only one instance sends the notification for each alert group. A typical production deployment runs three Alertmanager instances in a cluster, which provides both high availability and notification deduplication. The cluster uses the CRDT (Conflict-free Replicated Data Type) approach, meaning it is eventually consistent and can tolerate brief network partitions without losing alerts or sending duplicate notifications, which is critical for maintaining alert reliability during network instability.
| Feature | Purpose | Configuration | Production Recommendation |
|---|---|---|---|
| Grouping | Combine related alerts into one notification | group_by, group_wait, group_interval | Group by alertname + job |
| Inhibition | Suppress low-priority when high-priority fires | source_matchers, target_matchers | Inhibit service alerts on node failure |
| Silencing | Temporary alert suppression | Matchers + start/end time | Always set during maintenance windows |
| Routing | Direct alerts to correct teams/channels | Route tree with matchers | Severity-based + team-based routing |
| Clustering | HA + deduplication across instances | Gossip protocol cluster | 3-node cluster minimum for production |
9. Pushgateway and Short-Lived Jobs
The Pushgateway is a component that allows ephemeral and batch jobs to push their metrics into Prometheus's pull-based ecosystem. It serves as an intermediary storage buffer — short-lived jobs push their metrics to the Pushgateway before they terminate, and Prometheus scrapes the Pushgateway at its configured interval. The Pushgateway was designed to solve a genuine problem: in environments dominated by long-running services, Prometheus's pull model works perfectly because the services are always available for scraping. But batch jobs, cron jobs, CI/CD pipelines, and other short-lived processes complete in seconds or minutes, making it impossible for Prometheus to scrape them before they exit. The Pushgateway bridges this gap by providing a stable endpoint that accumulates metrics from multiple job runs.
9.1 Pushgateway Usage Pattern
The correct usage pattern for the Pushgateway is to push metrics from batch jobs just before they complete, using a job label and instance label to identify the specific job run. After the Pushgateway scrapes and Prometheus collects the data, the batch job should delete its metrics from the Pushgateway to prevent stale data from accumulating. This cleanup step is often overlooked, leading to a common anti-pattern where old batch job metrics persist in the Pushgateway indefinitely, creating confusion during debugging and artificially inflating the number of active time series that Prometheus must track and store.
C#
// C# example: Pushing metrics to Pushgateway from a batch job
using System.Net.Http;
using System.Text;
public class BatchJobMetrics
{
private const string PushgatewayUrl = "http://pushgateway:9091";
public static async Task PushMetricsAsync(string jobId)
{
var metrics = new StringBuilder();
metrics.AppendLine("# HELP batch_records_processed Total records processed");
metrics.AppendLine("# TYPE batch_records_processed counter");
metrics.AppendLine($"batch_records_processed{{job=\"data-pipeline\",run_id=\"{jobId}\"}} 15000");
metrics.AppendLine("# HELP batch_duration_seconds Batch processing duration");
metrics.AppendLine("# TYPE batch_duration_seconds gauge");
metrics.AppendLine($"batch_duration_seconds{{job=\"data-pipeline\",run_id=\"{jobId}\"}} 127.5");
metrics.AppendLine("# HELP batch_errors_total Total errors during batch");
metrics.AppendLine("# TYPE batch_errors_total counter");
metrics.AppendLine($"batch_errors_total{{job=\"data-pipeline\",run_id=\"{jobId}\"}} 3");
using var client = new HttpClient();
var content = new StringContent(metrics.ToString(), Encoding.UTF8, "text/plain");
await client.PutAsync(
$"{PushgatewayUrl}/metrics/job/data-pipeline/instance/{jobId}", content);
}
public static async Task DeleteMetricsAsync(string jobId)
{
using var client = new HttpClient();
await client.DeleteAsync(
$"{PushgatewayUrl}/metrics/job/data-pipeline/instance/{jobId}");
}
}
9.2 When NOT to Use the Pushgateway
The Pushgateway should NOT be used as a general-purpose metrics relay for long-running services. If your service runs continuously, Prometheus should scrape it directly — using the Pushgateway as an intermediary adds an unnecessary point of failure and doubles the network traffic. The Pushgateway should also not be used to buffer metrics when Prometheus is temporarily unavailable, because the Pushgateway holds metrics in memory and has no persistence layer. If the Pushgateway restarts, all pushed metrics are lost. For reliable metric buffering, consider using the Prometheus remote write capability with a durable backend like Thanos or Mimir, which provide persistent storage and guaranteed delivery semantics that the Pushgateway simply cannot match.
9.3 Alternatives to Pushgateway
In modern Kubernetes environments, many of the use cases traditionally served by the Pushgateway have been addressed by alternative patterns. For CronJobs, you can configure the job itself to expose a /metrics endpoint during its brief lifecycle and use Kubernetes annotation-based service discovery to scrape it. For serverless functions (AWS Lambda, Google Cloud Functions), the OpenTelemetry Collector with a Prometheus remote write exporter can push metrics to a Prometheus-compatible backend. These alternatives often provide better reliability and integration than the Pushgateway, though the Pushgateway remains useful in environments where direct scraping is not feasible and where the batch job cannot be modified to expose an HTTP endpoint for its entire duration.
| Scenario | Use Pushgateway? | Better Alternative | Reason |
|---|---|---|---|
| Long-running service | No | Direct scrape | Prometheus can scrape it directly |
| CronJob (K8s) | Maybe | K8s annotation-based scrape | Job can expose metrics endpoint |
| Short-lived batch job | Yes | Pushgateway | Job completes before scrape |
| CI/CD pipeline | Yes | Pushgateway | Pipeline is too ephemeral |
| Serverless function | No | OTel Collector + remote write | Better cloud integration |
| Prometheus outage buffer | No | Thanos / Mimir | Pushgateway has no persistence |
10. Exporters
Exporters are the bridge between the Prometheus monitoring model and the vast ecosystem of third-party systems that do not natively expose Prometheus-format metrics. An exporter runs as a separate process — typically as a sidecar, DaemonSet, or standalone deployment — that queries a target system's native metrics API and translates the results into the Prometheus exposition format. The Prometheus ecosystem has over 800 registered exporters covering everything from databases and message queues to hardware sensors and cloud provider APIs. Understanding which exporters to use, how to configure them, and when to write your own custom exporter is a critical skill for operating Prometheus at scale and ensuring comprehensive observability across your entire technology stack.
10.1 Node Exporter
The Node Exporter is the most widely deployed exporter and provides hardware and OS-level metrics for Linux and Unix systems. It exposes metrics for CPU usage, memory, disk I/O, network traffic, filesystem utilization, load averages, and dozens of other system-level metrics. In Kubernetes environments, the Node Exporter runs as a DaemonSet on every node, and Prometheus scrapes it to gain visibility into host-level health. The Node Exporter is highly configurable through command-line flags that enable or disable specific collectors — for example, you can disable the arp collector if you do not need ARP table metrics, reducing both CPU usage and the number of metrics exposed, which directly impacts the storage and query resources consumed by Prometheus.
10.2 Database Exporters
Database exporters — MySQL Exporter, PostgreSQL Exporter, MongoDB Exporter, Redis Exporter, and Elasticsearch Exporter — provide deep visibility into database performance. These exporters query the database's internal statistics tables (like MySQL's performance_schema or PostgreSQL's pg_stat_activity) and expose metrics such as query throughput, connection pool utilization, replication lag, cache hit ratios, and slow query counts. Properly configured database exporters are essential for identifying performance bottlenecks — a sudden increase in slow queries or a growing replication lag often precedes a database outage, making these metrics critical for proactive alerting and capacity planning.
10.3 Blackbox Exporter
The Blackbox Exporter probes external endpoints and measures the results — HTTP response codes, TLS certificate expiry, DNS resolution times, TCP connection establishment times, and ICMP ping latencies. Unlike other exporters that run alongside the target system, the Blackbox Exporter runs centrally and probes targets from Prometheus's perspective, simulating what an external user would experience. This makes it invaluable for monitoring SLO compliance, detecting certificate expiry, and verifying that services are reachable from outside the cluster. The Blackbox Exporter is configured through modules that define what type of probe to perform and what parameters to check, allowing you to create different probing configurations for different types of endpoints.
10.4 Custom Exporters
When no existing exporter meets your needs, writing a custom exporter is straightforward using client libraries in your language of choice. For .NET applications, the Prometheus.Client NuGet package provides both metric instrumentation and a middleware that automatically exposes a /metrics endpoint. For Go applications, the official prometheus/client_golang library is the standard choice. The key requirements for a custom exporter are: expose metrics in the Prometheus exposition format at a /metrics endpoint, use appropriate metric types (Counter for totals, Gauge for current values, Histogram for distributions), and follow Prometheus naming conventions with snake_case metric names and appropriate suffixes that clearly communicate what is being measured.
C#
// C# Custom Exporter: Exposing application-specific metrics
using Prometheus;
public class PaymentGatewayExporter
{
private static readonly Counter PaymentsProcessed = Prometheus.Metrics
.CreateCounter("payments_processed_total",
"Total number of payments processed",
new[] { "gateway", "currency", "status" });
private static readonly Histogram PaymentLatency = Prometheus.Metrics
.CreateHistogram("payment_processing_duration_seconds",
"Time spent processing payments",
new[] { "gateway" },
new HistogramConfiguration
{
Buckets = new[] { 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0 }
});
private static readonly Gauge GatewayHealth = Prometheus.Metrics
.CreateGauge("payment_gateway_health_status",
"Health status of payment gateway (1=healthy, 0=unhealthy)",
new[] { "gateway" });
public async Task ProcessPaymentAsync(PaymentRequest request)
{
var timer = PaymentLatency.WithLabels(request.Gateway).NewTimer();
try
{
var result = await _gateway.ProcessAsync(request);
PaymentsProcessed.WithLabels(request.Gateway, request.Currency, "success").Inc();
return result;
}
catch (PaymentDeclinedException)
{
PaymentsProcessed.WithLabels(request.Gateway, request.Currency, "declined").Inc();
throw;
}
catch (Exception)
{
PaymentsProcessed.WithLabels(request.Gateway, request.Currency, "error").Inc();
throw;
}
finally
{
timer.Dispose();
}
}
public void UpdateHealthStatus(string gateway, bool isHealthy)
{
GatewayHealth.WithLabels(gateway).Set(isHealthy ? 1 : 0);
}
}
| Exporter | Port | Source System | Key Metrics | Deployment Model |
|---|---|---|---|---|
| Node Exporter | 9100 | Linux/Unix hosts | CPU, memory, disk, network | DaemonSet (K8s) or systemd |
| MySQL Exporter | 9104 | MySQL/MariaDB | Queries, connections, replication | Sidecar or Deployment |
| PostgreSQL Exporter | 9187 | PostgreSQL | Stat activity, bgwriter, locks | Sidecar or Deployment |
| Redis Exporter | 9121 | Redis | Memory, connections, commands/sec | Sidecar or Deployment |
| MongoDB Exporter | 9216 | MongoDB | Operations, connections, oplog | Sidecar or Deployment |
| Blackbox Exporter | 9115 | External endpoints | HTTP status, TLS cert, DNS, TCP | Centralized Deployment |
| cAdvisor | 8080 | Docker containers | CPU, memory, network per container | Built into kubelet |
| Kube-State-Metrics | 8080 | Kubernetes API | Pod, deployment, node status | Deployment in monitoring ns |
11. Recording Rules and Alerting Rules
Rules are the backbone of a well-organized Prometheus deployment. Recording rules pre-compute expensive or frequently used PromQL expressions into new time series, dramatically improving query performance and enabling efficient dashboards. Alerting rules define the conditions under which Prometheus should fire alerts and send them to Alertmanager. Together, recording and alerting rules transform Prometheus from a passive metrics store into an active monitoring and alerting platform that continuously evaluates the health of your systems and provides proactive notifications when anomalies are detected, enabling teams to respond to issues before they impact users.
11.1 Recording Rules
Recording rules are defined in YAML files referenced by the rule_files configuration in prometheus.yml. Each recording rule specifies a PromQL expression and an output metric name. The rule evaluator periodically (default: every 15 seconds) evaluates the expression and writes the result to the output metric, which can then be queried efficiently. Recording rules are especially valuable for histogram quantile calculations, which involve scanning hundreds or thousands of bucket time series — pre-computing these as recording rules reduces dashboard load times from seconds to milliseconds. They also serve as a form of documentation, encoding the business logic of what a particular metric represents in a named, version-controlled output metric that can be referenced consistently across dashboards and alerts.
11.2 Alerting Rules
Alerting rules define conditions that trigger alerts. Each rule specifies a PromQL condition, a for duration (how long the condition must be continuously true before the alert fires), and labels that are attached to the alert. The for duration is critical for avoiding false positives from transient spikes — a disk usage alert with for: 5m will only fire if disk usage stays above the threshold for 5 consecutive minutes, preventing alert fatigue from brief, self-resolving fluctuations. When the condition becomes false, the alert transitions from "firing" back to "inactive", and Alertmanager stops sending notifications for it. The annotations field allows you to include dynamic information about the alert, such as the current metric value and links to runbooks, which are rendered in notification messages and the Alertmanager UI.
YAML
# rules.yml - Recording and Alerting Rules
groups:
- name: http_rules
interval: 30s
rules:
# Recording rules: pre-compute expensive aggregations
- record: http_requests:rate5m
expr: sum(rate(http_requests_total[5m])) by (job, method)
- record: http_request_duration:p99_5m
expr: >
histogram_quantile(0.99,
sum(rate(http_request_duration_seconds_bucket[5m])) by (le, job))
- record: http_request_duration:p95_5m
expr: >
histogram_quantile(0.95,
sum(rate(http_request_duration_seconds_bucket[5m])) by (le, job))
- record: http_errors:ratio_rate5m
expr: >
sum(rate(http_requests_total{status=~"5.."}[5m])) by (job)
/ sum(rate(http_requests_total[5m])) by (job)
# Alerting rules: fire alerts when conditions are met
- alert: HighErrorRate
expr: http_errors:ratio_rate5m > 0.05
for: 5m
labels:
severity: critical
team: platform
annotations:
summary: "High error rate for {{ $labels.job }}"
description: "Error rate is {{ $value | humanizePercentage }} (threshold: 5%)"
runbook_url: "https://wiki.internal/runbooks/high-error-rate"
- alert: HighP99Latency
expr: http_request_duration:p99_5m > 2.0
for: 10m
labels:
severity: warning
team: platform
annotations:
summary: "High p99 latency for {{ $labels.job }}"
description: "p99 latency is {{ $value }}s (threshold: 2s)"
- alert: ServiceDown
expr: up == 0
for: 1m
labels:
severity: critical
annotations:
summary: "{{ $labels.job }} is down"
description: "{{ $labels.instance }} unreachable for over 1 minute"
11.3 Rule Organization Patterns
In production, rules should be organized by domain or team rather than having one monolithic rules file. Common patterns include separating recording rules from alerting rules, organizing by service domain (infrastructure rules, application rules, business rules), and using recording rules to create building-block metrics that alerting rules reference. This layered approach makes rules easier to understand, test, and maintain. Many organizations use a template system or code generation to produce rules files from higher-level specifications, ensuring consistency across hundreds of services and preventing configuration drift that could lead to gaps in monitoring coverage or inconsistent alerting behavior across teams.
| Rule Type | Purpose | Evaluation | Output | Example |
|---|---|---|---|---|
| Recording Rule | Pre-compute expensive queries | Every evaluation_interval | New time series | http_request_duration:p99_5m |
| Alerting Rule | Detect anomalous conditions | Every evaluation_interval | Alert to Alertmanager | HighErrorRate, ServiceDown |
| Multi-target Recording | Aggregate across dimensions | Every evaluation_interval | Aggregated time series | sum(rate(...)) by (job) |
| Predictive Alerting | Forecast future problems | Every evaluation_interval | Predictive alert | DiskFullIn24Hours |
12. Remote Write and Long-Term Storage
Local Prometheus storage is designed for operational monitoring with typical retention periods of 15-30 days. However, many organizations need to retain metrics for months or years for capacity planning, compliance, cost analysis, and historical trend analysis. Remote write allows Prometheus to forward all collected metrics to an external long-term storage system in real-time, while still maintaining its local TSDB for fast queries over recent data. The remote write protocol uses an efficient protobuf-based encoding with Snappy compression, batching multiple samples into single HTTP POST requests to minimize network overhead. This dual-path architecture — local TSDB for fast recent queries and remote write for durable long-term storage — has become the standard pattern for production Prometheus deployments that need both operational responsiveness and historical depth.
12.1 Thanos
Thanos is a set of components that adds global query view, unlimited retention, and high availability to Prometheus. The key components are: Thanos Sidecar (runs alongside Prometheus and uploads TSDB blocks to object storage), Thanos Store Gateway (serves historical data from object storage), Thanos Compactor (downsamples and compacts blocks in object storage), Thanos Query (provides a PromQL-compatible query API that fans out to both sidecars and store gateways), and Thanos Receive (accepts remote write and creates TSDB blocks for storage). Thanos is designed as a loosely coupled set of components that can be deployed independently, giving operators flexibility to adopt only the components they need while maintaining a clear upgrade path as their requirements evolve from basic HA to full long-term storage capabilities.
12.2 Grafana Mimir
Grafana Mimir is a horizontally scalable, highly available, multi-tenant, long-term storage solution for Prometheus. Unlike Thanos, which is designed as a set of independent microservices, Mimir is a more opinionated, vertically integrated platform that provides a drop-in replacement for Prometheus's remote write target. Mimir uses a microservices architecture with Distributor, Ingester, Compactor, Querier, Ruler, and Store Gateway components, all coordinated through a hash ring stored in a KV store (Consul, etcd, or memberlist). Mimir's key differentiator is its native multi-tenancy support, which is essential for SaaS platforms or large organizations where different teams share the same monitoring infrastructure but require isolated query and storage views.
12.3 Remote Write Configuration
Configuring remote write requires setting the remote_write section in prometheus.yml with the target URL, queue configuration, and optional TLS/authentication settings. The queue configuration is critical for performance and reliability — the queue capacity determines how many samples can be buffered during network outages, and the max_samples_per_send controls batching efficiency. It is important to tune these parameters based on your metric volume and network conditions to avoid OOM kills or data loss during transient failures. The write_relabel_configs option allows you to filter which metrics are forwarded to long-term storage, reducing storage costs by excluding high-volume, low-value metrics that are only useful for real-time operational dashboards.
YAML
# prometheus.yml - Remote write configuration
remote_write:
- url: "http://thanos-receive:19291/api/v1/receive"
queue_config:
capacity: 10000
max_samples_per_send: 2000
batch_send_deadline: 5s
max_shards: 30
min_shards: 10
write_relabel_configs:
- source_labels: [__name__]
action: keep
regex: '(http_requests_total|http_request_duration_seconds|node_.*|up)'
send_exemplars: true
enable_http2: true
- url: "http://mimir-distributor:8080/api/v1/push"
name: mimir_remote
remote_timeout: 30s
queue_config:
capacity: 50000
max_samples_per_send: 5000
batch_send_deadline: 10s
max_shards: 50
tls_config:
cert_file: /etc/prometheus/certs/client.crt
key_file: /etc/prometheus/certs/client.key
ca_file: /etc/prometheus/certs/ca.crt
12.4 Choosing Between Thanos, Mimir, and VictoriaMetrics
The choice between Thanos, Mimir, and VictoriaMetrics depends on your requirements and operational preferences. Thanos is best for teams that want maximum flexibility and a loosely coupled architecture where they can adopt components incrementally. Mimir is best for teams that want a turnkey, horizontally scalable solution with native multi-tenancy and tight Grafana integration. VictoriaMetrics provides a Prometheus-compatible remote write target with superior compression and query performance, achieving 7-10x better compression than Prometheus's TSDB through its custom storage format, though it uses a proprietary storage engine that limits portability. Each option has its trade-offs, and the right choice depends on whether your priority is flexibility (Thanos), integration (Mimir), or performance (VictoriaMetrics).
| Feature | Thanos | Mimir | VictoriaMetrics | Cortex (deprecated) |
|---|---|---|---|---|
| Architecture | Loosely coupled microservices | Integrated platform | Single/cluster binary | Microservices |
| Multi-tenancy | Basic (via headers) | Native, production-grade | Basic | Native |
| Query Performance | Good (fan-out) | Excellent (caching) | Excellent | Good |
| Storage Format | Prometheus TSDB blocks | Prometheus TSDB blocks | Custom (higher compression) | Prometheus TSDB blocks |
| Downsampling | Yes (Compactor) | Yes (Compactor) | Native | Yes |
| Operational Complexity | Medium | Medium-High | Low | High |
| Best For | Incremental adoption | Grafana-native shops | Simplicity + performance | Legacy deployments |
13. Federation and Hierarchical Setup
Federation is Prometheus's native mechanism for scaling beyond a single server by creating a hierarchy of Prometheus instances. In a federated architecture, a global Prometheus server scrapes selected metrics from multiple leaf Prometheus servers, providing a unified query interface across the entire infrastructure. Federation is most useful when you need cross-cluster aggregation, hierarchical dashboards that show both global and per-cluster views, or when you want to decouple alerting from data collection — leaf servers collect all metrics locally, while the global server runs cross-cluster alerting rules that operate on aggregated data from multiple clusters, enabling you to detect patterns that would be invisible at the individual cluster level.
13.1 Federation Configuration
Federation is configured as a special scrape job on the global Prometheus server, using the honor_labels: true setting to preserve the original label values from the leaf servers. The federation endpoint on the leaf server is /federate, which accepts PromQL match[] parameters to filter which metrics are federated. This filtering is essential to avoid federating all metrics from every leaf server, which would quickly overwhelm the global server. Instead, you should federate only the metrics needed for cross-cluster aggregation — typically recording rules that have already been pre-aggregated on the leaf servers, ensuring that the federation traffic is manageable and the global server's resource consumption remains predictable regardless of the number of leaf clusters.
YAML
# Global Prometheus - federation scrape config
scrape_configs:
- job_name: 'federation'
honor_labels: true
metrics_path: /federate
params:
'match[]':
- '{__name__=~"http_requests:rate5m|http_request_duration:p99_5m|http_errors:ratio_rate5m"}'
- '{__name__=~"node_cpu_seconds_total|node_memory_MemAvailable_bytes"}'
- '{__name__=~"kube_.*", __name__!~"kube_pod_container_resource_.*"}'
static_configs:
- targets:
- 'prometheus-us-east:9090'
- 'prometheus-us-west:9090'
- 'prometheus-eu-west:9090'
scrape_interval: 60s
scrape_timeout: 30s
13.2 Federation vs. Remote Write
Federation and remote write serve different purposes and should not be confused. Federation is a pull-based approach where the global server actively scrapes the leaf servers — it is best for cross-cluster aggregation and global dashboards. Remote write is a push-based approach where leaf servers continuously stream all their data to a central store — it is best for long-term retention and centralized querying. In practice, many large deployments use both: federation for real-time global views with minimal latency and remote write for durable long-term storage in Thanos or Mimir that provides historical data access and cross-cluster query capabilities with higher latency but greater data completeness.
13.3 Hashmod Sharding
When a single Prometheus server cannot handle the scrape load for all targets in a large environment, hashmod sharding distributes targets across multiple Prometheus instances. The hashmod relabeling configuration assigns each target to a specific Prometheus instance based on a hash of the target address modulo the total number of instances. This ensures that each target is scraped by exactly one Prometheus instance, while the overall target set is evenly distributed. Hashmod sharding is configured at the service discovery level and requires each Prometheus instance to have a different hashmod value in its relabeling configuration. This approach is simpler than consistent hashing but requires resharding when instances are added or removed, making it less suitable for truly dynamic environments where the number of Prometheus instances changes frequently.
| Scaling Approach | Mechanism | Best For | Data Flow | Query Scope |
|---|---|---|---|---|
| Federation | Pull via /federate endpoint | Global dashboards, cross-cluster alerts | Global pulls from leaves | Global + per-cluster |
| Remote Write | Push to central storage | Long-term retention, centralized querying | Leaves push to central store | Centralized, full data |
| Hashmod Sharding | Hash-based target distribution | High scrape volume per target | Each shard scrapes subset | Shard-local, merge at query |
| Horizontal Scaling | Multiple independent instances | Multi-tenant, team-based isolation | Independent per instance | Instance-local |
14. Security
Prometheus was originally designed for trusted internal networks, and its default configuration ships without authentication or encryption. This was acceptable when Prometheus ran in a single cluster on a private network, but modern deployments — especially those spanning multiple clusters, clouds, or organizational boundaries — require robust security controls. Prometheus supports TLS for encrypted transport, bearer token and basic authentication for client authentication, and integration with external authorization systems through reverse proxies. Additionally, many of the ecosystem components (Thanos, Mimir) provide their own multi-tenant authentication layers that extend the security model beyond what native Prometheus offers, enabling secure operation in shared infrastructure environments where multiple teams coexist.
14.1 TLS Configuration
Every HTTP endpoint in Prometheus — the scrape targets, the query API, the Alertmanager connection, and the remote write connection — can be secured with TLS. TLS is configured through the tls_config section, which supports specifying CA certificates, client certificates, and minimum TLS versions. In production, you should always use TLS for remote write connections (which may traverse public networks), Alertmanager connections (which carry sensitive alert data), and the query API (which exposes operational metrics that could reveal vulnerability information to attackers). Scrape targets within a cluster may use mTLS via a service mesh like Istio or Linkerd instead of per-target TLS configuration, which simplifies certificate management and provides consistent encryption across all inter-service communication.
14.2 Authentication
Prometheus supports bearer token authentication and basic authentication natively. For more sophisticated authentication requirements — such as OAuth2, OIDC, or LDAP — you should deploy a reverse proxy (like OAuth2 Proxy or Pomerium) in front of Prometheus that handles authentication and passes the authenticated user's identity to Prometheus for authorization decisions. The --web.config.file flag enables a configuration file that supports both TLS and authentication settings in a single location, making it easy to enforce consistent security across all Prometheus endpoints without requiring changes to the main Prometheus configuration.
YAML
# web-config.yml - TLS and authentication for Prometheus
tls_server_config:
cert_file: /etc/prometheus/tls/server.crt
key_file: /etc/prometheus/tls/server.key
client_ca_file: /etc/prometheus/tls/ca.crt
min_version: TLS13
cipher_suites:
- TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
- TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
basic_auth_users:
admin: $2y$10$abcdefghijklmnopqrstuuABCDEFGHIJKLMNOPQRSTUVWXYZ0123
readonly: $2y$10$mnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012345abcdefghijklm
# prometheus.yml - TLS for scrape, remote write, and alertmanager
global:
scrape_configs:
- job_name: 'secure-targets'
scheme: https
tls_config:
cert_file: /etc/prometheus/tls/client.crt
key_file: /etc/prometheus/tls/client.key
ca_file: /etc/prometheus/tls/ca.crt
server_name: target.internal
remote_write:
- url: "https://thanos-receive:19291/api/v1/receive"
tls_config:
ca_file: /etc/prometheus/tls/ca.crt
cert_file: /etc/prometheus/tls/client.crt
key_file: /etc/prometheus/tls/client.key
alerting:
alertmanagers:
- scheme: https
tls_config:
ca_file: /etc/prometheus/tls/ca.crt
cert_file: /etc/prometheus/tls/client.crt
key_file: /etc/prometheus/tls/client.key
14.3 Network Security
Beyond application-level security, network-level controls are essential for Prometheus deployments. Prometheus should be deployed behind a network policy (in Kubernetes) or firewall rules (in VM environments) that restrict which hosts can access the Prometheus API and which targets Prometheus can scrape. The Prometheus API exposes sensitive operational data — service architecture, performance characteristics, and potential vulnerabilities — and should not be accessible from untrusted networks. Similarly, Prometheus should only scrape targets in its trust boundary, and scrape traffic should be restricted to private network interfaces. Network segmentation ensures that even if one component is compromised, the attacker cannot pivot to arbitrary targets within the infrastructure.
14.4 Secrets Management
Prometheus configuration files often contain sensitive information — scrape target credentials, remote write tokens, Alertmanager webhook secrets, and TLS private keys. These should never be stored in plaintext in configuration files or committed to version control. Instead, use Kubernetes Secrets (mounted as volumes), HashiCorp Vault (with the Prometheus Vault exporter), or cloud provider secret managers (AWS Secrets Manager, GCP Secret Manager). The Prometheus operator for Kubernetes simplifies secrets management by allowing you to reference Kubernetes Secrets directly in ServiceMonitor and PrometheusRule custom resources, reducing the attack surface and ensuring that secrets are rotated consistently across the entire monitoring infrastructure.
| Security Layer | Mechanism | Configuration | Priority |
|---|---|---|---|
| Transport Encryption | TLS 1.3 | tls_config in scrape_config | High - essential for remote write |
| Client Authentication | mTLS, Basic Auth, Bearer Token | basic_auth, bearer_token, tls_config | High - for API access |
| Reverse Proxy Auth | OAuth2 Proxy, Pomerium | External proxy in front of Prometheus | Medium - for web UI |
| Network Isolation | K8s NetworkPolicy, firewall rules | Ingress/egress rules | High - defense in depth |
| Secrets Management | K8s Secrets, Vault, cloud KMS | Volume mounts, env vars | High - no plaintext secrets |
| RBAC | Thanos/Mimir multi-tenancy | Tenant headers, access policies | Medium - for multi-tenant setups |
15. Performance Tuning
Performance tuning Prometheus is a multi-dimensional optimization problem that spans CPU, memory, disk I/O, and network usage. The key levers are: the number of active time series (which drives memory usage and TSDB performance), the scrape interval (which determines write throughput and data granularity), the block duration (which affects compaction overhead and query performance), and the retention policy (which determines total disk usage). Understanding the relationship between these parameters and their impact on system resources is essential for designing a Prometheus deployment that meets your performance requirements without exceeding your infrastructure budget, and for making informed trade-offs when resources are constrained.
15.1 Memory Tuning
Prometheus's memory usage is dominated by the head block, which holds all active time series in memory. Each active time series consumes approximately 2-4KB of RAM for the head block reference data, plus the memory for the active chunk (up to 8KB per chunk). A server with 5 million active time series will need approximately 20-30GB of RAM for the head block alone. Additional memory is consumed by the scrape buffers (which hold in-flight scrape responses), the query execution engine (which allocates temporary buffers proportional to the number of series being scanned), and the rule evaluation engine (which maintains state for in-progress rule evaluations). Monitoring the prometheus_tsdb_head_series metric provides real-time visibility into the active series count, which is the primary driver of memory consumption.
15.2 Disk I/O Tuning
The TSDB writes data in two phases: frequent small writes to the WAL and periodic large writes during head block flushes. The WAL writes benefit from fast random I/O (typically on SSDs), while the head block flushes benefit from fast sequential I/O. For production deployments, use dedicated SSD volumes for the TSDB data directory with no other processes sharing the I/O bandwidth. The WAL and the persistent blocks can be placed on separate volumes if I/O contention is observed. Monitor the TSDB's prometheus_tsdb_head_truncate_duration_seconds metric — if head block flushes are consistently slow (more than 10 seconds), it indicates I/O bottlenecks that need to be addressed before they cause data ingestion delays or increased memory pressure during the flush window.
15.3 Block Duration and Retention
The default 2-hour block duration is a balance between compaction overhead and query performance. Shorter block durations (e.g., 1 hour) create more blocks but reduce the head block flush latency. Longer block durations (e.g., 4 hours) reduce the number of blocks but increase the head block's memory footprint and flush latency. The retention period determines how many historical blocks are retained on disk. For local Prometheus, a 15-day retention is typical; for Thanos/Mimir backends, retention can be set to months or years. Use the --storage.tsdb.retention.time flag to set the retention period and monitor disk usage with the prometheus_tsdb_storage_blocks_bytes metric to ensure it stays within capacity, including sufficient headroom for compaction overhead and temporary space during block merges.
15.4 Query Optimization
PromQL query performance depends on the number of time series scanned, the time range queried, and the complexity of the aggregation. The most common performance pitfalls are: querying high-cardinality metrics over long time ranges (e.g., rate(http_requests_total[1h]) across 10 million series), using regex matchers on high-cardinality labels (e.g., {path=~".*"}), and computing histogram quantiles without pre-aggregated recording rules. To optimize queries, always use recording rules for expensive computations, limit the time range to what is actually needed, avoid regex matchers when exact matchers will suffice, and pre-aggregate series in recording rules so that dashboard queries operate on a small number of pre-computed time series rather than scanning millions of raw samples.
| Parameter | Default | Tuning Guidance | Impact |
|---|---|---|---|
| Block Duration | 2h | 1h for memory-constrained, 4h for I/O-constrained | Flush latency, query perf |
| Scrape Interval | 1m | 15s for critical, 30s for standard, 60s for batch | Storage, granularity, CPU |
| Retention Time | 15d | 15-30d local, 90d+ with remote write | Disk usage |
| WAL Compression | Disabled | Enable in all environments | 50% WAL disk reduction |
| Query Timeout | 2m | 30s-2m depending on query complexity | Query cancellation |
| Max Samples Per Query | 50M | Lower for shared instances | OOM prevention |
| TSDB Path | data/ | Dedicated SSD volume | I/O performance |
16. Comparison with InfluxDB, Graphite, and VictoriaMetrics
While Prometheus dominates the cloud-native monitoring space, it is not the only time series database and monitoring system available. Understanding how Prometheus compares to alternatives like InfluxDB, Graphite, and VictoriaMetrics helps engineers make informed architectural decisions and understand the trade-offs inherent in each approach. Each system was designed with different priorities — Prometheus prioritized pull-based collection and PromQL expressiveness, InfluxDB prioritized ease of use and push-based ingestion, Graphite prioritized simplicity and horizontal scalability, and VictoriaMetrics prioritized compression and query performance. These design philosophies shape every aspect of each system, from data ingestion to query optimization to operational characteristics.
16.1 Prometheus vs. InfluxDB
InfluxDB is a purpose-built time series database that uses a push-based collection model and its own query language (Flux, formerly InfluxQL). While InfluxDB is easier to set up initially and provides built-in support for push-based ingestion, Prometheus offers a richer query language (PromQL vs InfluxQL/Flux), better integration with the Kubernetes ecosystem, and a more mature alerting pipeline through Alertmanager. InfluxDB 3.0 (the latest version) has shifted to a columnar storage engine with significant performance improvements, but the Prometheus ecosystem — including Grafana dashboards, Alertmanager, Thanos, and Mimir — remains more comprehensive for cloud-native monitoring use cases, particularly in Kubernetes environments where Prometheus's service discovery capabilities provide a significant operational advantage.
16.2 Prometheus vs. Graphite
Graphite is one of the oldest time series databases, using a simple flat metric namespace (e.g., servers.web1.cpu.total.idle) rather than Prometheus's label-based model. While Graphite is extremely simple and scales well horizontally with its Carbon+Whisper architecture, its flat namespace makes multi-dimensional querying extremely difficult. You cannot easily answer questions like "what is the average CPU usage across all servers in the us-east region?" without explicitly naming every server in the query. Prometheus's label-based model handles this naturally with avg(node_cpu_seconds_total{region="us-east"}), making it far more flexible for modern, dynamic infrastructure where the set of monitored entities changes constantly.
16.3 Prometheus vs. VictoriaMetrics
VictoriaMetrics is a high-performance, cost-effective time series database that is fully compatible with Prometheus's remote write protocol and PromQL. It achieves 7-10x better compression than Prometheus's TSDB through its custom storage format, which uses combination encoding for metric names and labels and improved compression algorithms for sample values. VictoriaMetrics also provides built-in downsampling, multi-tenancy, and anomaly detection capabilities. The primary trade-off is that VictoriaMetrics uses a proprietary storage format — while it accepts Prometheus data via remote write, the stored data cannot be directly read by Prometheus, creating a degree of vendor lock-in that some organizations prefer to avoid, especially when long-term portability is a concern.
| Feature | Prometheus | InfluxDB | Graphite | VictoriaMetrics |
|---|---|---|---|---|
| Collection Model | Pull (primary) + Push | Push | Push (Carbon) | Remote write from Prometheus |
| Query Language | PromQL | Flux / InfluxQL | Render API / PromQL | PromQL (compatible) |
| Data Model | Metric + Labels | Measurement + Tags | Dot-delimited path | Metric + Labels |
| Storage Format | TSDB blocks (open) | TSM (proprietary) | Whisper (fixed-size) | Custom (proprietary) |
| Alerting | Alertmanager (mature) | Built-in (Kapacitor) | External (Grafana etc.) | Alerting via Alertmanager |
| K8s Integration | Excellent (native SD) | Good (Telegraf) | Limited | Excellent (Prom-compat) |
| Scalability | Federation / Thanos / Mimir | Enterprise clustering | Native horizontal (Carbon) | Cluster mode (VM cluster) |
| Best For | Cloud-native K8s monitoring | IoT, DevOps, general TS | Simple metrics, legacy | Cost-effective Prometheus alt |
17. Advanced C# Instrumentation Examples
This section provides comprehensive C# code examples demonstrating how to instrument .NET applications with Prometheus metrics using the Prometheus.Client library. These examples cover common enterprise scenarios including HTTP middleware for automatic request metrics, custom business metrics for domain-specific monitoring, health check integration, and runtime metrics collection. Proper instrumentation is the foundation of observability — without well-designed metrics, even the most sophisticated Prometheus deployment cannot provide meaningful insights into system behavior, making this section particularly valuable for .NET teams adopting Prometheus for the first time.
C#
// Complete ASP.NET Core Prometheus Integration Example
using Prometheus;
using Prometheus.Metrics;
using System.Diagnostics;
public class PrometheusMetricsSetup
{
public static readonly Histogram HttpRequestDuration = Prometheus.Metrics
.CreateHistogram("http_request_duration_seconds",
"Duration of HTTP requests in seconds",
new[] { "method", "endpoint", "status_code" },
new HistogramConfiguration
{
Buckets = new[] { 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10 }
});
public static readonly Counter HttpRequestTotal = Prometheus.Metrics
.CreateCounter("http_requests_total",
"Total number of HTTP requests",
new[] { "method", "endpoint", "status_code" });
// Business Metrics: Order Processing Pipeline
public static readonly Counter OrdersReceived = Prometheus.Metrics
.CreateCounter("orders_received_total",
"Total orders received",
new[] { "source", "region" });
public static readonly Counter OrdersCompleted = Prometheus.Metrics
.CreateCounter("orders_completed_total",
"Total orders successfully completed",
new[] { "region", "payment_method" });
public static readonly Counter OrdersFailed = Prometheus.Metrics
.CreateCounter("orders_failed_total",
"Total orders that failed processing",
new[] { "region", "failure_reason" });
public static readonly Histogram OrderProcessingDuration = Prometheus.Metrics
.CreateHistogram("order_processing_duration_seconds",
"Time to process an order from receipt to completion",
new[] { "region" },
new HistogramConfiguration
{
Buckets = new[] { 0.1, 0.5, 1, 2, 5, 10, 30, 60, 120 }
});
public static readonly Gauge ActiveOrderProcessors = Prometheus.Metrics
.CreateGauge("active_order_processors",
"Number of currently active order processing workers");
public static readonly Gauge OrderQueueDepth = Prometheus.Metrics
.CreateGauge("order_queue_depth",
"Current depth of the order processing queue",
new[] { "priority" });
// Infrastructure Metrics
public static readonly Gauge ActiveDatabaseConnections = Prometheus.Metrics
.CreateGauge("app_database_connections_active",
"Number of active database connections",
new[] { "pool" });
public static readonly Histogram ExternalApiCallDuration = Prometheus.Metrics
.CreateHistogram("external_api_call_duration_seconds",
"Duration of external API calls",
new[] { "service", "operation" },
new HistogramConfiguration
{
Buckets = new[] { 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10 }
});
public static readonly Counter ExternalApiCallTotal = Prometheus.Metrics
.CreateCounter("external_api_calls_total",
"Total external API calls",
new[] { "service", "operation", "status" });
// Cache Metrics
public static readonly Counter CacheOperationsTotal = Prometheus.Metrics
.CreateCounter("cache_operations_total",
"Total cache operations",
new[] { "operation", "result" });
public static readonly Gauge CacheSize = Prometheus.Metrics
.CreateGauge("cache_size_bytes",
"Current cache size in bytes");
}
public class OrderProcessingService
{
public async Task ProcessOrderAsync(Order order)
{
using var timer = PrometheusMetricsSetup.OrderProcessingDuration
.WithLabels(order.Region).NewTimer();
try
{
await ValidateOrderAsync(order);
await ProcessPaymentAsync(order);
await AllocateInventoryAsync(order);
await SendConfirmationAsync(order);
PrometheusMetricsSetup.OrdersCompleted
.WithLabels(order.Region, order.PaymentMethod).Inc();
}
catch (PaymentException)
{
PrometheusMetricsSetup.OrdersFailed
.WithLabels(order.Region, "payment_error").Inc();
throw;
}
catch (InventoryException)
{
PrometheusMetricsSetup.OrdersFailed
.WithLabels(order.Region, "inventory_error").Inc();
throw;
}
}
}
public class PrometheusMetricsMiddleware
{
private readonly RequestDelegate _next;
public PrometheusMetricsMiddleware(RequestDelegate next) => _next = next;
public async Task InvokeAsync(HttpContext context)
{
var sw = Stopwatch.StartNew();
try { await _next(context); }
finally
{
sw.Stop();
var method = context.Request.Method;
var endpoint = context.Request.Path.Value ?? "unknown";
var statusCode = context.Response.StatusCode.ToString();
PrometheusMetricsSetup.HttpRequestDuration
.WithLabels(method, endpoint, statusCode)
.Observe(sw.Elapsed.TotalSeconds);
PrometheusMetricsSetup.HttpRequestTotal
.WithLabels(method, endpoint, statusCode).Inc();
}
}
}
17.1 Metrics Naming Conventions in C#
When instrumenting .NET applications, follow Prometheus naming conventions: use snake_case for metric names, include the base unit as a suffix (_seconds, _bytes, _total), and keep metric names descriptive but concise. The Prometheus.Client library automatically adds the _total suffix to Counter metrics, so you should not include it in the name passed to CreateCounter(). Similarly, Histogram and Summary metric names should not include the unit suffix in the label names — the suffix on the metric name itself is sufficient to communicate the unit, and including it in both the name and labels creates redundancy that makes metric names unnecessarily verbose and harder to query.
C#
// Health check integration with Prometheus metrics
using Microsoft.Extensions.Diagnostics.HealthChecks;
public class PrometheusHealthCheckPublisher : IHealthCheckPublisher
{
private static readonly Gauge HealthCheckStatus = Prometheus.Metrics
.CreateGauge("app_health_check_status",
"Health check status (1=healthy, 0=unhealthy)",
new[] { "check_name" });
private static readonly Histogram HealthCheckDuration = Prometheus.Metrics
.CreateHistogram("app_health_check_duration_seconds",
"Duration of health check execution",
new[] { "check_name" },
new HistogramConfiguration
{
Buckets = new[] { 0.01, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5 }
});
public Task PublishAsync(HealthReport report, CancellationToken ct)
{
foreach (var entry in report.Entries)
{
HealthCheckStatus.WithLabels(entry.Key)
.Set(entry.Value.Status == HealthStatus.Healthy ? 1 : 0);
HealthCheckDuration.WithLabels(entry.Key)
.Observe(entry.Value.Duration.TotalSeconds);
}
return Task.CompletedTask;
}
}
C#
// Database connection pool metrics using EF Core interception
using Microsoft.EntityFrameworkCore.Diagnostics;
public class DatabaseMetricsInterceptor : DbCommandInterceptor
{
private static readonly Histogram DbQueryDuration = Prometheus.Metrics
.CreateHistogram("db_query_duration_seconds",
"Duration of database queries",
new[] { "database", "operation" },
new HistogramConfiguration
{
Buckets = new[] { 0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1 }
});
private static readonly Counter DbQueryTotal = Prometheus.Metrics
.CreateCounter("db_queries_total",
"Total number of database queries executed",
new[] { "database", "operation", "status" });
public override async ValueTask> ScalarExecutingAsync(
DbCommand command, CommandEventData eventData,
InterceptionResult result, CancellationToken ct = default)
{
var sw = Stopwatch.StartNew();
try
{
var outcome = await base.ScalarExecutingAsync(
command, eventData, result, ct);
sw.Stop();
DbQueryDuration.WithLabels("app_db", "scalar").Observe(sw.Elapsed.TotalSeconds);
DbQueryTotal.WithLabels("app_db", "scalar", "success").Inc();
return outcome;
}
catch (Exception ex)
{
sw.Stop();
DbQueryDuration.WithLabels("app_db", "scalar").Observe(sw.Elapsed.TotalSeconds);
DbQueryTotal.WithLabels("app_db", "scalar", "error").Inc();
throw;
}
}
}
// Register in Program.cs:
// builder.Services.AddDbContext(options =>
// options.AddInterceptors(new DatabaseMetricsInterceptor()));
18. Interview Q&A
The following questions cover the most commonly asked Prometheus design and architecture topics in senior and staff-level engineering interviews. These questions go beyond surface-level knowledge and test your understanding of internal mechanisms, trade-offs, and operational considerations. For each question, we provide a concise, structured answer that demonstrates the depth expected at the senior+ level. Mastering these answers will not only help you ace interviews but also deepen your understanding of how to design and operate Prometheus in production environments where reliability, performance, and scalability are critical requirements.
Q1: Why does Prometheus use a pull-based model instead of push?
The pull-based model provides several advantages: it eliminates the need for applications to know about the monitoring infrastructure (a significant operational benefit in large organizations), it allows Prometheus to detect whether targets are alive simply by whether the scrape succeeds (providing built-in health checking), it provides natural backpressure (Prometheus can slow down scraping if it is overloaded without losing data), and it enables Prometheus to be the single source of truth for what metrics are collected. The pull model also simplifies deployment — you do not need to configure push endpoints or worry about push reliability. The trade-off is that very short-lived jobs cannot be scraped before they terminate, which is where the Pushgateway fills the gap for the small percentage of workloads that require push-based collection.
Q2: What is the difference between rate() and irate() in PromQL?
The rate() function computes the per-second rate of increase using a least-squares regression over the entire time window (e.g., the last 5 minutes), which smooths out spikes and provides a stable rate suitable for alerting and dashboards. The irate() function computes the instantaneous rate using only the last two samples in the range, making it highly responsive to spikes but also very noisy. Use rate() for alerting rules where you need stability and want to avoid false positives from brief spikes. Use irate() for interactive dashboards where you want to see fine-grained rate changes. Never use irate() in alerting rules because its sensitivity to individual sample pairs makes it unreliable for detecting sustained anomalies.
Q3: How would you design Prometheus for a multi-cluster Kubernetes deployment?
The recommended architecture uses a hub-and-spoke model: each cluster runs its own Prometheus instance(s) that collect all local metrics via Kubernetes service discovery and standard scrape configurations. For global querying and long-term storage, use Thanos or Mimir as the centralized backend. Each cluster's Prometheus uploads blocks to a shared object storage bucket (S3/GCS), and a central Thanos Query or Mimir Querier provides a unified query interface across all clusters. Alerting rules can be evaluated at both the cluster level (for cluster-specific alerts) and the global level (for cross-cluster alerts like regional capacity warnings). This architecture provides local resilience (each cluster can operate independently if the global backend is unavailable) while enabling global visibility and long-term retention.
Q4: What causes high memory usage in Prometheus and how do you troubleshoot it?
High memory usage is typically caused by: (1) too many active time series, which increases the head block size proportionally — diagnose with prometheus_tsdb_head_series; (2) expensive PromQL queries that scan millions of series without pre-aggregated recording rules — diagnose with prometheus_engine_query_duration_seconds; (3) large scrape responses that require significant buffer memory — diagnose with prometheus_target_sync_length and scrape duration metrics; (4) memory leaks in recording or alerting rules that maintain per-series state. The primary mitigation is to reduce cardinality by dropping high-cardinality labels via metric relabeling, adding recording rules to pre-aggregate expensive queries, and scaling horizontally by sharding targets across multiple Prometheus instances using hashmod relabeling.
Q5: Explain the Thanos architecture and when you would choose it over Mimir.
Thanos consists of loosely coupled components: Sidecar (uploads blocks, serves recent data), Store Gateway (serves historical data from object storage), Compactor (downsamples and compacts), Query (fan-out query engine), and Receive (accepts remote write). Choose Thanos when you want incremental adoption — you can start with just Sidecar for HA and add Store Gateway and Compactor later. Choose Mimir when you need native multi-tenancy, better query performance through built-in caching, and tighter Grafana integration. Mimir is more opinionated and operationally complex to set up initially, but provides a more complete out-of-the-box experience. Thanos is better for teams that want flexibility and control over individual components, while Mimir is better for teams that want a turnkey solution with Grafana Labs support.
Q6: How do you handle metric cardinality explosions in production?
Cardinality explosions occur when labels with unbounded values (like user IDs, request IDs, or URLs) create millions of time series. Prevention is the best strategy: implement code review processes that flag high-cardinality labels before they reach production, use metric relabeling to drop or aggregate high-cardity metrics at the Prometheus level, and set sample_limit and target_limit in scrape configurations. When a cardinality explosion occurs in production, use the topk(10, count by (__name__)({__name__=~".+"})) query to identify which metrics have the most series, then immediately add metric relabeling rules to drop the offending metrics. Long-term, implement a metrics catalog or schema registry that defines allowed label values and enforces cardinality budgets per team or service.
Q7: What is the significance of the for clause in alerting rules?
The for clause specifies the minimum duration that an alert condition must be continuously true before the alert actually fires. This is critical for preventing false positives from transient spikes — for example, a CPU usage alert with for: 5m will only fire if CPU usage stays above the threshold for 5 consecutive minutes, filtering out brief spikes caused by garbage collection, deployment rollouts, or other temporary conditions. Without the for clause, every brief threshold violation would trigger an alert, leading to alert fatigue and eroding trust in the monitoring system. The for duration should be tuned based on the expected variability of the metric — stable metrics like disk usage can use shorter durations (1-2m), while variable metrics like CPU or latency may need longer durations (5-10m) to avoid noise.
Q8: How do you implement SLOs (Service Level Objectives) using Prometheus?
Implementing SLOs in Prometheus involves three steps: (1) Define error budget as the acceptable ratio of failed requests to total requests (e.g., 99.9% availability means 0.1% error budget); (2) Create recording rules that compute the SLI (Service Level Indicator) as a ratio of good events to total events over a rolling window (e.g., sum(rate(http_requests_total{status!~"5.."}[30d])) / sum(rate(http_requests_total[30d]))); (3) Create alerting rules that fire when the error budget is being consumed too quickly (e.g., if the 30-day error ratio exceeds 50% of the budget, the alert fires indicating the team will exhaust its error budget before the end of the measurement window). Use the Prometheus SLO recording rules library (prometheus/slo-libsonnet or pyrra) for battle-tested SLO implementations that handle edge cases like cold starts, missing data, and multi-window burn rate alerts.
Q9: What are exemplars and how do they connect metrics to traces?
Exemplars are metadata attached to metric samples that provide a link to a specific trace or request ID. For example, a Histogram sample for http_request_duration_seconds_bucket{le="0.5"} might include an exemplar with a trace ID like {trace_id="abc123"}, allowing you to click from a high-latency metric spike in Grafana directly to the specific trace that caused it. Exemplars bridge the gap between metrics (which tell you something is slow) and traces (which tell you why it is slow). They are configured at the instrumentation level using the Prometheus client libraries and are transmitted via the remote write protocol (when send_exemplars: true is configured) and stored in Thanos/Mimir for correlation queries. This metrics-to-traces correlation is a key capability of modern observability platforms and represents the convergence of the three pillars of observability: metrics, traces, and logs.
Q10: Design a Prometheus monitoring system for a payment processing platform with strict latency and reliability requirements.
The design would include: (1) Three Prometheus instances per region in an active-active configuration with hashmod sharding to distribute scrape load; (2) Alertmanager three-node cluster for HA alert routing with PagerDuty integration for critical alerts; (3) Thanos or Mimir for long-term storage with 90-day retention for compliance and capacity planning; (4) Custom instrumentation using C# Prometheus client library tracking payment latency, success rate, gateway health, circuit breaker state, and queue depth with carefully bounded labels; (5) Recording rules for all dashboard queries and multi-window burn rate alerts for SLOs (99.99% availability target for payment processing); (6) Blackbox Exporter probing payment gateway endpoints from multiple regions; (7) Network-level isolation with mTLS between all Prometheus components; (8) Remote write with write_relabel_configs to filter high-cardinality debug metrics from long-term storage. The critical alert path should have less than 2 minutes from condition detection to on-call notification, achieved through 1-minute scrape intervals on payment-critical metrics, 30-second rule evaluation intervals, and 30-second group_wait in Alertmanager.