+++
title = "Usage and metrics"
description = "A cluster's storage and document counts over time, its request metrics, and the search terms its users send most."
weight = 89
updated = 2026-11-03

[extra]
nav_title = "Usage and metrics"
weight = 50
+++

<!-- Generated by script/build-api-reference.js from data/openapi/v1.yml and data/api-reference/pages.yml. Edit those and run `npm run build:api-reference`, not this file. -->

A cluster's storage and document counts over time, its request metrics,
and the search terms its users send most. For the current totals only,
read `stats` on the [cluster](@/docs/api/reference/clusters/index.md#cluster-object).
When the store behind these doesn't answer, they return
`503 metrics_unavailable`, never an empty list, so an empty list always
means there's no data.

**On this page:** [Show a cluster's usage](#get-cluster-usage), [Show a cluster's request metrics](#get-cluster-metrics), [Show a cluster's top queries](#get-cluster-top-queries).

## Show a cluster's usage {#get-cluster-usage}

<p class="api-endpoint"><span class="api-method api-method-get">GET</span> <code>/v1/clusters/{cluster_id}/usage</code></p>

- **Scope:** `clusters:read`

One sample per index-stats poll (about every 10 minutes), summed over
the cluster's indices, oldest first. At most 90 days. A cluster that
was never polled has an empty `samples`, not a zero sample. The
cluster's current totals are [`stats`](@/docs/api/reference/clusters/index.md#cluster-object) on the Cluster, not part of this body.

### Parameters

| Name | In | Type | Description |
|---|---|---|---|
| `cluster_id` | path | string | Required. The cluster's ID, which is its slug. It never changes. |
| `from` | query | string (date-time) | Start of the range, RFC 3339. Defaults to one hour before `to`. |
| `to` | query | string (date-time) | End of the range, RFC 3339. Defaults to now. |

### Example request

```bash
curl "https://api.bonsai.io/v1/clusters/my-search-4823919/usage?from=2026-10-26T00%3A00%3A00Z&to=2026-10-26T12%3A00%3A00Z" \
  -H "Authorization: Bearer $BONSAI_TOKEN"
```

### Response

`200 OK`. Usage samples.

| Field | Type | Description |
|---|---|---|
| `from` | string (date-time) | Start of the range. |
| `to` | string (date-time) | End of the range. |
| `samples` | array of objects | Oldest first. Empty when the cluster was never polled in the range. |
| `samples[].at` | string (date-time) | When the sample was taken. |
| `samples[].bytes` | integer | Bytes on disk. |
| `samples[].docs` | integer | Documents. |
| `samples[].docs_deleted` | integer | Deleted documents not yet merged away. |
| `samples[].shards` | integer | Shards, primaries and replicas. |
| `samples[].primary_shards` | integer | Primary shards. |
| `samples[].health` | string | One of `green`, `yellow`, `red`. More may be added. Worst index health. |

```json
{
  "from": "2026-10-26T00:00:00Z",
  "to": "2026-10-26T12:00:00Z",
  "samples": [
    {
      "at": "2026-10-26T00:04:00Z",
      "bytes": 48103424,
      "docs": 12810,
      "docs_deleted": 31,
      "shards": 2,
      "primary_shards": 1,
      "health": "green"
    },
    {
      "at": "2026-10-26T00:14:00Z",
      "bytes": 48211968,
      "docs": 12840,
      "docs_deleted": 31,
      "shards": 2,
      "primary_shards": 1,
      "health": "green"
    }
  ]
}
```

### Errors

| Status | Codes |
|---|---|
| `400` | [`invalid_parameter`](@/docs/api/errors/index.md#invalid_parameter) |
| `401` | [`unauthorized`](@/docs/api/errors/index.md#unauthorized) |
| `403` | [`missing_scope`](@/docs/api/errors/index.md#missing_scope) |
| `404` | [`not_found`](@/docs/api/errors/index.md#not_found) |
| `429` | [`rate_limited`](@/docs/api/errors/index.md#rate_limited), [`too_many_failed_authentications`](@/docs/api/errors/index.md#too_many_failed_authentications) |
| `503` | [`metrics_unavailable`](@/docs/api/errors/index.md#metrics_unavailable) |

## Show a cluster's request metrics {#get-cluster-metrics}

<p class="api-endpoint"><span class="api-method api-method-get">GET</span> <code>/v1/clusters/{cluster_id}/metrics</code></p>

- **Scope:** `clusters:read`

Bucketed request metrics. Without `resolution`, the server picks the
smallest of 1m, 30m, 1h, 6h, 12h, 1d that gives fewer than 100 buckets
(30 days gives 12h). At most 30 days and 1,000 buckets, and `from` no
earlier than the start of the UTC day 30 days ago. A range out of
bounds is a 400 naming `from`, and a resolution not offered or giving
too many buckets is a 400 naming `resolution`.

### Parameters

| Name | In | Type | Description |
|---|---|---|---|
| `cluster_id` | path | string | Required. The cluster's ID, which is its slug. It never changes. |
| `from` | query | string (date-time) | Start of the range, RFC 3339. Defaults to one hour before `to`. |
| `to` | query | string (date-time) | End of the range, RFC 3339. Defaults to now. |
| `resolution` | query | string | Bucket size, one of 1m, 30m, 1h, 6h, 12h, 1d. |

### Example request

```bash
curl "https://api.bonsai.io/v1/clusters/my-search-4823919/metrics?from=2026-10-26T00%3A00%3A00Z&to=2026-10-26T12%3A00%3A00Z&resolution=1h" \
  -H "Authorization: Bearer $BONSAI_TOKEN"
```

### Response

`200 OK`. Metric buckets.

| Field | Type | Description |
|---|---|---|
| `from` | string (date-time) | Start of the range. |
| `to` | string (date-time) | End of the range. |
| `resolution` | string | The bucket size asked for, or the one the server picked. |
| `buckets` | array of objects | One per `resolution`, oldest first. |
| `buckets[].at` | string (date-time) | When the bucket starts. |
| `buckets[].requests` | integer | Requests in the bucket. |
| `buckets[].writes` | integer | Write requests in the bucket. |
| `buckets[].responses` | object | Responses by HTTP status. |
| `buckets[].responses.status_2xx_3xx` | integer | Responses with a 2xx or 3xx status. |
| `buckets[].responses.status_4xx` | integer | Responses with a 4xx status. |
| `buckets[].responses.status_5xx` | integer | Responses with a 5xx status. |
| `buckets[].duration_ms` | object | Percentiles for this bucket. null when there were no requests. |
| `buckets[].duration_ms.p50` | number or null | Median request duration. |
| `buckets[].duration_ms.p95` | number or null | 95th percentile request duration. |
| `buckets[].duration_ms.p99` | number or null | 99th percentile request duration. |
| `buckets[].duration_histogram` | array of objects | Request counts by duration, always 17 buckets in this order. Each starts at its `min_ms` and ends where the next starts: 0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 200, 300, 400, 500, 1000 and 2000 (no upper bound). |
| `buckets[].duration_histogram[].min_ms` | integer | Where the bucket starts, in milliseconds. |
| `buckets[].duration_histogram[].count` | integer | Requests that took that long. |
| `buckets[].bytes_sent` | integer | Bytes the cluster sent to clients. |
| `buckets[].bytes_received` | integer | Bytes the cluster received from clients. |
| `buckets[].queue_time_ms` | number | Time requests waited at Bonsai's load balancer, in milliseconds. Ideally 0. See [Dashboard metrics](@/docs/features/dashboard-metrics/index.md). |
| `buckets[].concurrency` | number | Requests running at the same time. Queue time rises when this reaches the plan's limit. |

```json
{
  "from": "2026-10-26T00:00:00Z",
  "to": "2026-10-26T12:00:00Z",
  "resolution": "1h",
  "buckets": [
    {
      "at": "2026-10-26T00:00:00Z",
      "requests": 4210,
      "writes": 380,
      "responses": { "status_2xx_3xx": 4188, "status_4xx": 22, "status_5xx": 0 },
      "duration_ms": { "p50": 12.4, "p95": 48.1, "p99": 96.7 },
      "duration_histogram": [
        { "min_ms": 0, "count": 1210 },
        { "min_ms": 10, "count": 1530 },
        { "min_ms": 20, "count": 640 },
        { "min_ms": 30, "count": 310 },
        { "min_ms": 40, "count": 190 },
        { "min_ms": 50, "count": 120 },
        { "min_ms": 60, "count": 70 },
        { "min_ms": 70, "count": 45 },
        { "min_ms": 80, "count": 30 },
        { "min_ms": 90, "count": 20 },
        { "min_ms": 100, "count": 35 },
        { "min_ms": 200, "count": 7 },
        { "min_ms": 300, "count": 2 },
        { "min_ms": 400, "count": 1 },
        { "min_ms": 500, "count": 0 },
        { "min_ms": 1000, "count": 0 },
        { "min_ms": 2000, "count": 0 }
      ],
      "bytes_sent": 18432000,
      "bytes_received": 2048000,
      "queue_time_ms": 0.4,
      "concurrency": 1.2
    }
  ]
}
```

### Errors

| Status | Codes |
|---|---|
| `400` | [`invalid_parameter`](@/docs/api/errors/index.md#invalid_parameter) |
| `401` | [`unauthorized`](@/docs/api/errors/index.md#unauthorized) |
| `403` | [`missing_scope`](@/docs/api/errors/index.md#missing_scope) |
| `404` | [`not_found`](@/docs/api/errors/index.md#not_found) |
| `429` | [`rate_limited`](@/docs/api/errors/index.md#rate_limited), [`too_many_failed_authentications`](@/docs/api/errors/index.md#too_many_failed_authentications) |
| `503` | [`metrics_unavailable`](@/docs/api/errors/index.md#metrics_unavailable) |

## Show a cluster's top queries {#get-cluster-top-queries}

<p class="api-endpoint"><span class="api-method api-method-get">GET</span> <code>/v1/clusters/{cluster_id}/queries</code></p>

- **Scope:** `query_logs:read`

The top 100 normalized search strings in a window, with volume and
averages. These are the customer's end users' search terms, so the
token needs `query_logs:read`.

### Parameters

| Name | In | Type | Description |
|---|---|---|---|
| `cluster_id` | path | string | Required. The cluster's ID, which is its slug. It never changes. |
| `window` | query | string | One of 1h, 1d, 7d, 14d, 30d. Defaults to 7d. |

### Example request

```bash
curl "https://api.bonsai.io/v1/clusters/my-search-4823919/queries?window=7d" \
  -H "Authorization: Bearer $BONSAI_TOKEN"
```

### Response

`200 OK`. Top queries.

| Field | Type | Description |
|---|---|---|
| `window` | string | The window asked for. |
| `window_start_at` | string (date-time) | When the window starts. |
| `window_end_at` | string (date-time) | When the window ends. |
| `computed_at` | string (date-time) | When the list was computed. |
| `queries` | array of objects | Up to 100 search strings, most frequent first. |
| `queries[].query` | string | Lowercased, quotes stripped. |
| `queries[].volume` | integer | How many times it was searched. |
| `queries[].avg_duration_ms` | number | Average search duration, in milliseconds. |
| `queries[].avg_hits` | number | Average number of hits. |

```json
{
  "window": "7d",
  "window_start_at": "2026-10-19T14:00:00Z",
  "window_end_at": "2026-10-26T14:00:00Z",
  "computed_at": "2026-10-26T14:05:00Z",
  "queries": [
    {
      "query": "running shoes",
      "volume": 1840,
      "avg_duration_ms": 14.2,
      "avg_hits": 312
    },
    {
      "query": "rain jacket",
      "volume": 960,
      "avg_duration_ms": 11.8,
      "avg_hits": 87
    }
  ]
}
```

### Errors

| Status | Codes |
|---|---|
| `400` | [`invalid_parameter`](@/docs/api/errors/index.md#invalid_parameter) |
| `401` | [`unauthorized`](@/docs/api/errors/index.md#unauthorized) |
| `403` | [`feature_not_enabled`](@/docs/api/errors/index.md#feature_not_enabled), [`missing_scope`](@/docs/api/errors/index.md#missing_scope) |
| `404` | [`not_found`](@/docs/api/errors/index.md#not_found) |
| `429` | [`rate_limited`](@/docs/api/errors/index.md#rate_limited), [`too_many_failed_authentications`](@/docs/api/errors/index.md#too_many_failed_authentications) |
| `503` | [`metrics_unavailable`](@/docs/api/errors/index.md#metrics_unavailable) |
