Optimizing Query Performance
1. Using Recording Rules
Recording rules allow you to precompute frequently used or expensive queries and store the results as new time series. This can significantly improve query performance by reducing the amount of data Prometheus needs to scan during queries.
| Field | Description | Example |
|---|---|---|
| record | Name of the new time series to be created | http_request_duration_seconds_bucket |
| expr | PromQL expression to evaluate and store | rate(http_request_duration_seconds_bucket[5m]) |
| labels | Optional labels to add to the recorded metric | {job="api-server"} |
Example: Recording Rule Configuration
groups:
- name: http_rules
rules:
- record: http_request_duration_seconds_bucket
expr: rate(http_request_duration_seconds_bucket[5m])
labels:
job: api-server
Note: Recording rules are evaluated at the same interval as the scrape interval, or less frequently if configured. They help reduce query load on Prometheus by caching results.
Warning: Avoid creating too many recording rules as they can increase memory usage and potentially cause performance issues.
2. Limiting Query Range
| Option | Values | Description |
|---|---|---|
| range | 1h, 1d, 1w |
Defines the time range for the query. Example: rate(cpu_usage{job="web"}[5m]) for the last 5 minutes. |
| start | 2024-01-01T00:00:00Z |
Start time for the query. Optional, defaults to current time minus range. |
| end | 2024-01-01T01:00:00Z |
End time for the query. Optional, defaults to current time. |
| step | 15s, 1m |
Step size for the query. Determines how often to sample data. |
Note: Using
@ with a timestamp limits the query to that specific time window. This is useful for historical analysis or debugging.3. Avoiding High-Cardinality Queries
| Query Pattern | Description | Impact | Best Practice |
|---|---|---|---|
| count_values() | Counts occurrences of each value for a label | High cardinality if label has many unique values | Use topk() or quantile() instead |
| label_values() | Lists all values for a label | Can return thousands of unique values | Avoid in queries; use group_left or group_right with known values |
| rate(metric{label=~".*"}) | Matches all values with regex | Can cause high cardinality and slow queries | Use specific label values or count() to limit scope |
Note: High-cardinality queries can cause Prometheus to use excessive memory and CPU, leading to degraded performance or out-of-memory errors.
4. Using Efficient Selectors
| Selector Type | Description | Performance Impact | Best Practice |
|---|---|---|---|
| metric_name | Matches all time series with the given name | Fastest - no label matching | Use when you only need the metric name |
| metric_name{label="value"} | Matches time series with the given name and label value | Moderate - requires label matching | Use specific labels to reduce result set size |
| metric_name{label=~"regex"} | Matches time series with the given name and label matching regex | Slow - regex evaluation on all matching series | Avoid unless necessary; use exact matches when possible |
| metric_name{label!~"regex"} | Matches time series with the given name and label NOT matching regex | Very slow - evaluates all series | Avoid; consider filtering at ingestion or using alternative queries |
Example: Efficient Selector Usage
# Fast query - no labels
up
# Efficient query - specific label match
http_requests_total{method="GET"}
# Less efficient - regex matching on labels
http_requests_total{method=~"POST|PUT"}
# Inefficient - negative regex match (avoid)
http_requests_total{method!~"GET|POST"}
Note: Label values should be kept as short and stable as possible to improve query performance.
Warning: Using regex patterns in label matching can significantly increase query execution time, especially on large datasets.
5. Implementing Query Timeouts
| Configuration Option | Default Value | Description |
|---|---|---|
| query.timeout | 2m |
Maximum time a query is allowed to run before being cancelled |
| query.max-samples | 50000000 |
Maximum number of samples a single query is allowed to load |
Example: Setting Query Timeout in Prometheus Configuration
query:
timeout: 30s
max-samples: 100000000
Note: Setting a query timeout prevents long-running queries from consuming excessive resources and affecting other queries.
Warning: Setting the timeout too low may cause valid queries to be prematurely cancelled, especially for complex aggregations or large datasets.
6. Configuring Max Samples
| Option | Values | Notes |
|---|---|---|
| max_samples | 1000000 |
Maximum number of samples a single query can return |
| max_samples_per_query | 1000000 |
Alias for max_samples, deprecated in v2.30+ |
Example: Setting max_samples in Prometheus configuration
global:
scrape_interval: 15s
evaluation_interval: 15s
rule_files:
- "prometheus.rules"
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
- job_name: 'node'
static_configs:
- targets: ['localhost:9100']
# Configure max samples
query:
max_samples: 5000000
Note: Setting
max_samples too low can cause queries to be truncated, while setting it too high can lead to memory exhaustion or query timeouts.
7. Using Subqueries Efficiently
| Subquery Type | Description | Best Practices | Notes |
|---|---|---|---|
| Instant Vector Subqueries | Queries that return a single value at a specific timestamp | Use with rate() or increase() for counter metrics |
Can be inefficient if not scoped properly |
| Range Vector Subqueries | Queries that return a vector over a time range | Use sum_over_time(), avg_over_time() for aggregations |
More resource-intensive than instant queries |
| Aggregation Subqueries | Queries that aggregate data over time windows | Use group_left, group_right for joins |
Can cause high memory usage if not optimized |
Note: Subqueries can significantly impact query performance. Always test with
query_range endpoint to measure latency.
Warning: Avoid nested subqueries as they can lead to exponential query execution times.
8. Avoiding Regex Overuse
| Pattern | Description | Impact | Better Alternative |
|---|---|---|---|
| /.*foo.*/ | Matches any string containing "foo" | Scans entire string for each match | /.*foo.*/ → count_foo |
| /.*foo.*/ | Matches any string containing "foo" | Can cause performance issues on large datasets | count_foo or sum by (foo) |
Note: Regex patterns in queries can be expensive and should be avoided when direct metric filtering is possible.
9. Implementing Result Caching
Example: Using Prometheus Caching with Grafana
prometheus:
enable_remote_write: true
remote_write:
- url: "http://grafana-agent:9090/api/prom/push"
queue_config:
capacity: 1000
max_shards: 10
min_shard_size: 100
| Configuration Option | Description | Default Value | Notes |
|---|---|---|---|
| enable_remote_write | Enables remote write support for caching results | false | Required to enable caching |
| remote_write.url | URL to send cached metrics to | None | Must be a valid remote write endpoint |
| queue_config.capacity | Maximum number of samples in the queue | 1000 | Higher values allow more buffering |
| queue_config.max_shards | Maximum number of shards for parallel processing | 10 | Higher values increase concurrency |
| queue_config.min_shard_size | Minimum size of each shard in the queue | 100 | Controls batch size for writes |
Note: Caching is implemented via remote write to a caching agent or storage system.
Warning: Ensure that the remote write endpoint supports caching to avoid performance degradation.
Best Practices for Caching
- Use
queue_configto optimize batch sizes and shard counts - Monitor queue capacity to prevent data loss during high load
- Configure
enable_remote_writeto enable caching features
10. Monitoring Query Performance
Example: Using Prometheus UI to Monitor Query Performance
# Query to check query duration
rate(prometheus_engine_query_duration_seconds_sum[5m]) / rate(prometheus_engine_query_duration_seconds_count[5m])
# Example result:
# {instance="localhost:9090",job="prometheus"} 0.002345
| Metric | Description | Unit | Notes |
|---|---|---|---|
| prometheus_engine_query_duration_seconds_sum | Total time spent evaluating queries in seconds | seconds | Used to calculate average query duration |
| prometheus_engine_query_duration_seconds_count | Number of queries evaluated | count | Used to calculate average query duration |
| prometheus_engine_query_duration_seconds_bucket | Bucketed query durations for histogram | seconds | Used to analyze query performance distribution |
Note: The query duration metrics are exposed by Prometheus itself and can be used to monitor the performance of queries being executed.
Example: Querying Query Duration Histogram
# Query to get 99th percentile of query duration
histogram_quantile(0.99, sum by(le) (rate(prometheus_engine_query_duration_seconds_bucket[5m])))
# Example result:
# {instance="localhost:9090",job="prometheus"} 0.056789
Query Performance Monitoring Best Practices
- Monitor query duration using
prometheus_engine_query_duration_seconds_sumandprometheus_engine_query_duration_seconds_count - Use histogram quantiles to understand query performance distribution
- Set up alerts for queries that exceed a certain duration threshold
- Use
rate()function to calculate query rates over time windows
11. Using Lookback Delta
| Property | Description | Default Value | Notes |
|---|---|---|---|
| lookback_delta | Time range to look back for data when evaluating a query | 5m |
Controls how far back Prometheus searches for data points when evaluating a query. Affects query performance and accuracy. |
Note: Increasing
lookback_delta can improve accuracy for long-running queries but may increase memory usage and query latency.
Warning: Setting
lookback_delta too high can lead to excessive memory consumption and slow query execution.
12. Optimizing Aggregations
| Aggregation Function | Description | Performance Notes | Example |
|---|---|---|---|
| sum() | Sums values across series | Can be expensive with many time series. Use sum by (label) to group |
sum by (job) (node_cpu_seconds_total) |
| avg() | Averages values across series | Similar to sum; consider using avg by (label) |
avg by (instance) (http_requests_total) |
| max() | Maximum value across series | Efficient, but grouping can reduce overhead | max by (job) (up) |
| min() | Minimum value across series | Similar to max; use grouping for efficiency | min by (job) (up) |
| count() | Counts number of values | Fast, but can be slow if used with many series | count by (job) (up) |
| stddev() | Standard deviation of values | Computationally intensive; avoid on large datasets | stddev by (job) (node_cpu_seconds_total) |
| quantile() | Quantile (e.g., 0.95) of values | Expensive; consider using histogram_quantile() for histograms |
quantile by (job) (0.95, http_request_duration_seconds) |
Note: Use
group_left, group_right carefully to avoid expensive joins.Warning: Aggregations over large numbers of time series can cause high memory usage and slow query execution.