Skip to main content
A Histogram is a synchronous instrument that records a distribution of values. Histograms are ideal for understanding the statistical distribution of measurements like request duration, response sizes, or temperatures.

When to Use Histogram

Use a Histogram when:
  • You need to calculate percentiles (p50, p95, p99)
  • You want to understand the distribution of values
  • Min, max, sum, and count are all valuable
  • Values represent measurements or observations
Common examples:
  • HTTP request duration
  • Response payload size
  • Database query latency
  • Temperature readings
  • Request processing time
Histograms are more expensive than counters because they record individual measurements and calculate statistics. Use them only when you need distribution data.

API Reference

pub struct Histogram<T>(Arc<dyn SyncInstrument<T> + Send + Sync>);

impl<T> Histogram<T> {
    pub fn record(&self, value: T, attributes: &[KeyValue]);
}
The record method adds a value to the distribution.

Creating a Histogram

Histograms support u64 and f64 data types:
use opentelemetry::global;

let meter = global::meter("my-app");

// f64 histogram (most common)
let duration_histogram = meter.f64_histogram("http_request_duration")
    .with_description("HTTP request duration in seconds")
    .with_unit("s")
    .build();

// u64 histogram
let size_histogram = meter.u64_histogram("response_size")
    .with_description("HTTP response size in bytes")
    .with_unit("By")
    .build();

Recording Measurements

Use the record method to add values to the histogram:
use opentelemetry::KeyValue;

// Record a single value
duration_histogram.record(0.123, &[]);

// Record with attributes
duration_histogram.record(
    0.456,
    &[
        KeyValue::new("method", "GET"),
        KeyValue::new("endpoint", "/api/users"),
        KeyValue::new("status", "200"),
    ],
);

// Record response sizes
size_histogram.record(1024, &[KeyValue::new("content_type", "application/json")]);

Configuring Bucket Boundaries

Histogram accuracy depends on bucket boundaries. You can customize them when creating the histogram:
let histogram = meter.f64_histogram("request_duration")
    .with_description("Request duration in seconds")
    .with_unit("s")
    .with_boundaries(vec![
        0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0
    ])
    .build();

Default Boundaries

If you don’t specify boundaries, the default is:
vec![0.0, 5.0, 10.0, 25.0, 50.0, 75.0, 100.0, 250.0, 500.0, 750.0, 1000.0, 2500.0, 5000.0, 7500.0, 10000.0]

Boundary Requirements

Boundaries must:
  • Not contain f64::NAN, f64::INFINITY, or f64::NEG_INFINITY
  • Be in strictly increasing order
  • Not contain duplicate values
Invalid boundaries will cause the instrument to not report measurements.

Choosing Boundaries

Choose boundaries based on your expected value range:
// For request duration in seconds (most requests < 1s)
let latency = meter.f64_histogram("latency")
    .with_boundaries(vec![0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0])
    .build();

// For response size in bytes
let size = meter.u64_histogram("response_size")
    .with_boundaries(vec![100.0, 500.0, 1000.0, 5000.0, 10000.0, 50000.0, 100000.0])
    .build();

// For fine-grained millisecond measurements
let ms_latency = meter.f64_histogram("db_query_duration")
    .with_unit("ms")
    .with_boundaries(vec![1.0, 2.0, 5.0, 10.0, 20.0, 50.0, 100.0, 200.0, 500.0, 1000.0])
    .build();
Use more buckets for ranges where you expect most values. This improves percentile accuracy but increases memory and network usage.

Complete Example

use opentelemetry::{global, KeyValue};
use opentelemetry_sdk::metrics::SdkMeterProvider;
use opentelemetry_sdk::Resource;
use std::time::Instant;

fn init_meter_provider() -> SdkMeterProvider {
    let exporter = opentelemetry_stdout::MetricExporterBuilder::default().build();
    let provider = SdkMeterProvider::builder()
        .with_periodic_exporter(exporter)
        .with_resource(
            Resource::builder()
                .with_service_name("my-service")
                .build(),
        )
        .build();
    global::set_meter_provider(provider.clone());
    provider
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let meter_provider = init_meter_provider();
    let meter = global::meter("http-server");

    // Create a histogram for request duration
    let histogram = meter
        .f64_histogram("http_request_duration")
        .with_description("HTTP request duration in seconds")
        .with_unit("s")
        .with_boundaries(vec![
            0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0
        ])
        .build();

    // Simulate handling a request
    let start = Instant::now();
    // ... process request ...
    let duration = start.elapsed().as_secs_f64();

    histogram.record(
        duration,
        &[
            KeyValue::new("method", "GET"),
            KeyValue::new("endpoint", "/api/users"),
            KeyValue::new("status", "200"),
        ],
    );

    meter_provider.shutdown()?;
    Ok(())
}

Example from Source

From the metrics-basic example:
// Create a Histogram Instrument
let histogram = meter
    .f64_histogram("my_histogram")
    .with_description("My histogram example description")
    .with_boundaries(vec![0.0, 5.0, 10.0, 15.0, 20.0, 25.0])
    .build();

// Record measurements
histogram.record(
    10.5,
    &[
        KeyValue::new("mykey1", "myvalue1"),
        KeyValue::new("mykey2", "myvalue2"),
    ],
);

Exponential Histograms

For unpredictable value ranges, use exponential histograms via Views. They automatically adjust bucket widths:
use opentelemetry_sdk::metrics::{Aggregation, Instrument, Stream};

let exponential_histogram_view = |i: &Instrument| {
    if i.name() == "my_histogram" {
        Stream::builder()
            .with_aggregation(Aggregation::Base2ExponentialHistogram {
                max_size: 160,
                max_scale: 20,
                record_min_max: true,
            })
            .build()
            .ok()
    } else {
        None
    }
};

let provider = SdkMeterProvider::builder()
    .with_view(exponential_histogram_view)
    .build();
See Views for more details on customizing aggregation.

Attributes and Cardinality

Like counters, histograms support attributes:
histogram.record(
    0.123,
    &[
        KeyValue::new("method", "GET"),
        KeyValue::new("status", "200"),
    ],
);
Each unique attribute combination creates a separate histogram. Keep cardinality low to avoid memory issues.

Cloning Histograms

Histograms can be cloned to share across your application:
let histogram = meter.f64_histogram("latency").build();
let histogram_clone = histogram.clone();

// Both record to the same underlying histogram
histogram.record(0.1, &[]);
histogram_clone.record(0.2, &[]);
Clone histograms instead of creating duplicates. Multiple histograms with the same name can lower SDK performance.

Histogram vs. Gauge

// Use for distributions - captures min, max, percentiles
let histogram = meter.f64_histogram("request_duration").build();
histogram.record(0.123, &[]);
histogram.record(0.456, &[]);
histogram.record(0.789, &[]);
// Results: min, max, p50, p95, p99, count, sum

Best Practices

  1. Choose appropriate boundaries: Match your expected value range
  2. Use consistent units: Seconds for duration, bytes for size
  3. Keep cardinality low: Limit unique attribute combinations
  4. Clone when sharing: Don’t create duplicate histograms
  5. Use f64 for most cases: Better precision for measurements
  6. Consider exponential histograms: For unpredictable ranges

Next Steps

Counter

Record monotonically increasing values

Gauge

Record independent point-in-time values

Views

Customize histogram aggregation and boundaries

Observable Instruments

Use callbacks to report measurements

Build docs developers (and LLMs) love