> ## Documentation Index
> Fetch the complete documentation index at: https://metoro.io/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# KEDA Autoscaling

<Warning>
  **Alpha.** KEDA autoscaling is undergoing internal testing at Metoro. You are welcome to use it in a testing capacity, but do not rely on it for production autoscaling just yet. We expect breaking changes to the interface until around the 7th of August 2026.
</Warning>

Metoro can drive [KEDA](https://keda.sh) autoscaling: any query you can chart in Metoro (metric, trace, or log based) can drive a KEDA `ScaledObject`, so your workloads scale on the same signals you already monitor and alert on. There is no need to run a separate Prometheus or push metrics to another system.

The integration uses KEDA's built-in [`prometheus` scaler](https://keda.sh/docs/latest/scalers/prometheus/): Metoro serves a Prometheus-compatible query endpoint that evaluates MetoroQL, so you configure a standard trigger type your team may already know, your API key rides in a `TriggerAuthentication`.

<Note>
  The `prometheus` trigger is a stopgap: we plan to contribute a native `metoro` scaler type to KEDA, and once it merges upstream and reaches the KEDA release you run, trigger configuration becomes a first-class `type: metoro` with its own parameters. The endpoint behind it stays the same, existing `prometheus` triggers keep working, and this page will document the migration when the time comes.
</Note>

## Prerequisites

* KEDA installed in your cluster ([KEDA deployment docs](https://keda.sh/docs/latest/deploy/)).
* A Metoro API key. We recommend a dedicated key per cluster, bound to a read-only telemetry role (created in step 1 below).

## Setup

### 1. Create a read-only telemetry role, group, and API key

The scaler only ever evaluates queries, so the key it uses needs no resource permissions at all: a role with nothing but telemetry read access follows least privilege, and if the key ever leaks it cannot read dashboards or alerts, change settings, or touch anything else in Metoro. Permissions flow role → group → key, so you create all three:

1. **Create the role.** In **Settings → Users & Groups → Roles**, click **Create Role** (say, `keda-autoscaler`):
   * Grant **no resource permissions**. Custom roles are deny-by-default, so leaving them empty means the key can do nothing except what you grant next.
   * Under **Telemetry Permissions**, grant the signals your scaling queries use: `metrics` for metric queries, plus `traces` or `logs` if you scale on trace- or log-derived signals. If you do not yet know what you will scale on, just select all the signal types; the role is still read-only, and you can narrow it later.
   * Optionally scope the grants with match rules (for example to one namespace or environment). The scaler evaluates queries under the key's telemetry scope, so a scoped role means scaling queries can only see the telemetry you granted, and a query outside that scope simply evaluates against no data.
2. **Create a group bound to the role.** API keys are assigned to groups, not roles directly. In the **Groups** tab, create a group (say, `keda-autoscalers`) and bind it to the `keda-autoscaler` role only.
3. **Create the API key.** From the [API keys page](https://us-east.metoro.io/settings?tab=developer), create the key assigned to just that group.

See [Access Control (RBAC)](/docs/user-management/rbac) for the full permissions model.

### 2. Store your API key in a Secret

```yaml theme={null}
apiVersion: v1
kind: Secret
metadata:
  name: metoro-api-key
stringData:
  apiKey: metoro_secret_...
```

### 3. Create a TriggerAuthentication

KEDA reads the key from the Secret and sends it to Metoro as a bearer token on every request. The key never appears in your ScaledObject specs, and rotating it is just updating the Secret.

```yaml theme={null}
apiVersion: keda.sh/v1alpha1
kind: TriggerAuthentication
metadata:
  name: metoro-trigger-auth
spec:
  secretTargetRef:
    - parameter: bearerToken
      name: metoro-api-key
      key: apiKey
```

### 4. Create a ScaledObject

```yaml theme={null}
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: checkout-scaler
spec:
  scaleTargetRef:
    name: checkout
  minReplicaCount: 1
  triggers:
    - type: prometheus
      metadata:
        serverAddress: https://scaler.us-east.metoro.io   # on-prem: http://scaler.<namespace>.svc:8080
        query: 'count(traces{server.service.name="/k8s/production/checkout"})'
        queryParameters: language=metoroql,window=60,reduce=avg
        threshold: "100"
        timeout: "15000"   # milliseconds
        authModes: bearer
      authenticationRef:
        name: metoro-trigger-auth
```

This example scales on request rate observed from traces: the count of requests hitting the checkout service, with no instrumentation in the service itself. The value KEDA receives is the request count per evaluation bucket (60 seconds by default), averaged over the window, so `threshold: "100"` means "aim for one replica per 100 requests per minute". Keep the query an **aggregate total** like this (a total count, total connections, queue depth): the HPA divides the value across replicas itself under its default metric type, so per-pod averages produce wrong scaling math.

`minReplicaCount: 1` is deliberate, not a placeholder. KEDA's default is 0, and a signal emitted by the workload itself (like this one: the traces come from the checkout service serving requests) cannot wake a workload from zero, because at zero replicas there is nothing left to emit it. Only set `minReplicaCount: 0` with a demand signal measured upstream of the workload that keeps reporting explicit zeros while it is absent, such as queue depth in a broker.

The query is the same MetoroQL the Metoro UI generates, written verbatim, so the easiest way to build one is to chart the signal in the [metric explorer](https://us-east.metoro.io/metric-explorer), confirm it looks right, and copy the query. What you see on the chart is exactly what KEDA scales on.

## Trigger metadata reference

The standard `prometheus` scaler fields:

| Field                 | Required | Default          | Meaning                                                                                                                                                                                              |
| --------------------- | -------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `serverAddress`       | yes      |                  | `https://scaler.us-east.metoro.io` for Metoro cloud, `http://scaler.<namespace>.svc:8080` on-premises. KEDA calls `{serverAddress}/api/v1/query`                                                     |
| `query`               | yes      |                  | A MetoroQL query, verbatim. Must produce a single series; a multi-series result is rejected naming the offending labels, so aggregate across series in the query itself (e.g. wrap it in `sum(...)`) |
| `queryParameters`     | yes      |                  | Metoro-specific parameters, comma-separated `key=value` pairs; see the table below. At minimum `language=metoroql,window=<seconds>`                                                                  |
| `threshold`           | yes      |                  | The target the HPA scales against                                                                                                                                                                    |
| `activationThreshold` | no       | `0`              | The workload is considered active by KEDA when the value exceeds this                                                                                                                                |
| `timeout`             | no       | KEDA global (3s) | Per-trigger HTTP timeout in milliseconds; set `"15000"` to cover the synchronous first evaluation (KEDA versions before 2.20 reject duration strings here)                                           |
| `authModes`           | yes      |                  | Set to `bearer`                                                                                                                                                                                      |

The Metoro parameters inside `queryParameters`:

| Parameter     | Required | Default | Meaning                                                                                                                                                                                                                                          |
| ------------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `language`    | yes      |         | The query language. Always explicit, never guessed: `metoroql` is the only supported value today, and other values (including `promql`) are rejected, because many strings are valid in both languages with different semantics                  |
| `window`      | yes      |         | Evaluation window in seconds. The query is evaluated over this trailing window, aligned to complete buckets: the window is floored to a whole number of buckets ending on a bucket boundary, so partially-filled buckets never dilute the signal |
| `reduce`      | no       | `avg`   | How the datapoints in the window collapse to the single value KEDA receives: `avg`, `max`, `min`, `sum`, or `last`                                                                                                                               |
| `environment` | no       | all     | Restrict the query to one environment                                                                                                                                                                                                            |
| `bucket`      | no       | `60`    | Evaluation granularity in seconds within the window; the datapoints at this granularity are what `reduce` collapses. Clamped to the window                                                                                                       |

## Debugging with curl

The endpoint speaks the Prometheus instant-query shape, so you can see exactly what KEDA sees:

```bash theme={null}
curl -sG https://scaler.us-east.metoro.io/api/v1/query \
  -H "Authorization: Bearer $METORO_API_KEY" \
  --data-urlencode 'query=count(traces{server.service.name="/k8s/production/checkout"})' \
  --data-urlencode 'language=metoroql' \
  --data-urlencode 'window=60'
```

A healthy signal returns one sample whose timestamp is the evaluation time, so you can also read how fresh the value driving your autoscaling is:

```json theme={null}
{"status":"success","data":{"resultType":"vector","result":[{"metric":{},"value":[1753799000.123,"142.5"]}]}}
```

Failures are never empty results or zeroes: they are HTTP error statuses with a JSON body naming the problem (see failure semantics below), and the same message appears in the KEDA operator log and in `kubectl describe scaledobject`.

## Failure semantics

* **Missing data is an error, not zero.** If the window contains no datapoints, the endpoint returns `404` with `errorType: no_datapoints` rather than reporting 0. Many metrics are emitted infrequently or in batches, so an empty window often just means the next datapoint has not arrived yet; treating that as a literal 0 would scale your workload to minimum between emissions. Erroring instead lets the HPA hold replicas until data appears. If your metric is emitted infrequently, size `window` comfortably larger than the emission interval so the window always contains datapoints.
* **Stale data is an error.** Metoro serves the scaling signal from a cache refreshed on KEDA's polling cadence. An interruption in refresh is invisible for a while (the cached value keeps serving, holding your scale steady); past roughly ten minutes of failed refreshes the signal returns `503` with `errorType: stale_data` instead of serving fiction.
* **Errors hold your current replica count.** Any non-2xx response is a scaler error to KEDA: the HPA cannot compute a desired replica count, so it keeps the workload at whatever size the last good signal produced. This is the default and usually what you want: capacity tracks the last real measurement through an outage.
* **`fallback` replaces holding with a pinned count; use it deliberately.** KEDA's [`fallback`](https://keda.sh/docs/latest/concepts/scaling-deployments/#fallback) engages after `failureThreshold` consecutive scaler errors and converges the workload to `fallback.replicas`, in both directions: a workload that was at 20 replicas when the signal failed gets scaled *down* to a fallback of 6. Configure it only if you specifically want a fixed known-safe capacity during outages (for example, if an outage coinciding with being scaled low worries you more than tracking the last measurement), and size it as safe capacity, not as a minimum. It also only applies to `AverageValue` metrics (the default).

## On-premises

The scaler ships in the Metoro on-prem chart, disabled by default. Enable it in your values:

```yaml theme={null}
scaler:
  enabled: true
```

KEDA then reaches it in-cluster at `serverAddress: http://scaler.<metoro-namespace>.svc:8080` (plain HTTP, no TLS parameters needed). An optional ingress (`scaler.ingress` values) exposes it to KEDA operators in other clusters.

## Troubleshooting

| Response                         | Meaning                                                                                                                                                                                                                         |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `401` `unauthenticated`          | The TriggerAuthentication is not attached, `authModes: bearer` is missing, the Secret key name is wrong, or the API key expired. Check the key on the [API keys page](https://us-east.metoro.io/settings?tab=developer)         |
| `400` `bad_data`                 | A parameter problem: missing `language` or `window` in `queryParameters`, an unknown parameter (typos are rejected rather than ignored), or a query that does not parse. The message names the exact problem                    |
| `404` `no_datapoints`            | The query is valid but matched no data: check the metric name, filters, and `environment`, and confirm the signal is visible in the metric explorer                                                                             |
| `422` `unprocessable_query`      | The query returns multiple series or a non-finite value; aggregate in the query, for example `sum(...)`                                                                                                                         |
| `503` `pending_first_evaluation` | Normal on the first poll of a new query; KEDA retries on its next poll and the signal is served from then on                                                                                                                    |
| `503` `stale_data`               | Refreshes have been failing (the error includes the last refresh error); the HPA holds the current replica count, or your `fallback` engages if you configured one                                                              |
| First poll times out client-side | KEDA's default HTTP timeout (3s) is shorter than the synchronous first evaluation; set `timeout: "15000"` (milliseconds) on the trigger. The evaluation completes server-side regardless and the next poll is served from cache |
