Skip to content

Chart histograms as percentiles or a heatmap

A histogram metric records a distribution rather than a single number. Once you send one, the metrics explorer can render it two ways beyond the usual line chart:

  • Percentiles plots p50, p90, p95 and p99 over time.
  • Heatmap plots the whole distribution, with bucket ranges on one axis and time on the other, so you can see a bimodal shape that a p95 line hides.

Pick either from the View control in the query bar. Both modes need a histogram-type metric; a gauge or counter has no distribution to draw.

Histograms use Prometheus-style cumulative buckets. Each bucket’s count is the number of observations at or below its le bound, so counts only ever increase across the array.

Terminal window
curl -X POST https://metrics.siteqwality.com/metrics \
-H "Authorization: Bearer $SITEQWALITY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"metrics": [
{
"type": "histogram",
"name": "http.request.duration",
"timestamp": "2026-08-02T12:00:00Z",
"buckets": [
{ "le": "0.1", "count": 24 },
{ "le": "0.5", "count": 55 },
{ "le": "1.0", "count": 78 },
{ "le": "+Inf", "count": 100 }
],
"sum": 43.21,
"count": 100,
"unit": "seconds",
"tags": { "host": "web-1", "env": "prod" }
}
]
}'

Rules the ingest endpoint enforces, each a 400 for the whole batch if broken:

RuleDetail
Buckets must not be emptyAt least one bucket.
The last bucket must be +InfSpelled exactly +Inf. inf, Inf and +inf are all rejected.
Bounds must ascend strictlyNo duplicates, no out-of-order bounds.
Max 1000 metrics per requestBatch larger payloads.
Name charset^[a-zA-Z0-9._]+$. No hyphens, colons or slashes.

sum and count are required alongside the buckets. timestamp defaults to ingest time; unit and tags are optional. A successful call returns 202 Accepted and the write happens asynchronously.

Terminal window
curl -G https://api.siteqwality.com/metrics/histogram \
-H "Authorization: Bearer $SITEQWALITY_API_KEY" \
--data-urlencode "metric_name=http.request.duration" \
--data-urlencode "start_time=2026-08-02T00:00:00Z" \
--data-urlencode "end_time=2026-08-02T12:00:00Z" \
--data-urlencode "resolution=5m"
ParamDefaultNotes
metric_namerequired
start_timeone hour before end_timeStrict RFC 3339.
end_timenowStrict RFC 3339.
resolutionautoauto, 1m, 5m, 1h, 6h. Anything else silently falls back to auto.
filtersnoneJSON object of tag filters.

One response drives both views:

{
"bounds": ["0.1", "0.5", "1", "+Inf"],
"buckets": [
{
"timestamp": "2026-08-02T12:00:00Z",
"counts": [24, 31, 23, 22],
"total": 100,
"sum": 43.21,
"p50": 0.42, "p90": 0.87, "p95": 0.93, "p99": 0.99
}
],
"unit": "seconds",
"interval_seconds": 300
}

counts are per-bucket (not cumulative) so a heatmap can render them directly. The percentile fields drive the percentile view.

  • Intervals with no data are absent, not zero. A gap in the response means nothing was recorded, and the chart shows a gap rather than a floor.
  • Percentiles are per interval and cannot be averaged. Each bucket’s percentiles come from that interval’s own merged distribution. Averaging p99 across intervals is not a meaningful p99.
  • Histogram data is retained for 90 days.
  • Keep one bucket layout per metric name. Rows for the same metric are merged bucket-by-bucket per interval. If two services emit the same metric name with different bounds, one layout wins and the merged result is not meaningful. Give them different names or a distinguishing tag.
  • Timestamps here are second-precision. Do not rely on sub-second alignment when correlating with other charts.