# Scale Adapter: custom scaling targets

`ScaleAdapter` (`autoscaling.kedify.io/v1alpha1`, short name `sca`) lets KEDA and the Kubernetes HPA scale resources whose `/scale` subresource is incomplete, or that have no `/scale` subresource at all. It is available starting with Kedify Agent `v0.6.8`.

Warm node capacity through the adapter: capacity buffer guides for [Karpenter](https://docs.kedify.io/how-to/karpenter-capacity-buffers/), [Cluster Autoscaler](https://docs.kedify.io/how-to/cluster-autoscaler-capacity-buffers/), and [GKE](https://docs.kedify.io/how-to/gke-capacity-buffers/). The [Agones Fleet walkthrough](https://docs.kedify.io/features/scale-adapter/#usage-with-an-agones-fleet) below covers the incomplete `/scale` case.

## When You Need It

KEDA can target any custom resource that exposes the Kubernetes `/scale` subresource. Some operators, however, expose a partially functional implementation: desired replicas can be read and written, but the scale selector is missing or empty.

An [Agones Fleet](https://agones.dev/site/docs/reference/fleet/#fleet-scale-subresource-specification) exposes `/scale` for replica changes. If its response omits `status.selector`, a KEDA-generated HPA reports `ScalingActive=False` with reason `InvalidSelector`. ScaleAdapter supplies the selector. Check the response for your installed Agones version:

```bash
kubectl get --raw /apis/agones.dev/v1/namespaces/NAMESPACE/fleets/FLEET/scale
```

The adapter sits between the HPA and the target and normalizes the scale contract:

[![KEDA and HPA write desired replicas through ScaleAdapter, which forwards them to a target scale endpoint or configured replica fields.](https://docs.kedify.io/assets/images/docs/scale-adapter-contract.svg)](https://docs.kedify.io/assets/images/docs/scale-adapter-contract.svg)

Scroll to exploreDiagram description

KEDA supplies external metrics to HPA and handles activation and scale to zero. HPA writes active replica counts to the ScaleAdapter scale subresource. The adapter exposes replicas and a selector, then forwards the desired count to a target scale subresource or configured replica field paths. It mirrors target status; it does not make scaling decisions.

KEDA and the HPA keep all their responsibilities: metric evaluation, min/max bounds, stabilization, and scaling behavior. The adapter only exposes an HPA-compatible `/scale` subresource with an explicit Pod selector, forwards desired replica changes to the target’s `/scale` endpoint, and mirrors the target’s current replicas back. Any KEDA or Kedify trigger works, including scale to zero.

## Enabling

Enable the controller on the Kedify Agent helm chart and grant RBAC for the target kind’s `/scale` subresource, which is not part of the default role:

```yaml
agent:
  features:
    scaleAdaptersEnabled: true
  extraRbacRules:
    - apiGroups: ["agones.dev"]
      resources: ["fleets/scale"]
      verbs: ["get", "update"]
```

The env var equivalent of the feature toggle is `SCALE_ADAPTER_ENABLED` (default `false`). In this default mode the controller only uses the target’s `/scale` endpoint; it never needs update permission on the whole target resource. The exception is [targets without a `/scale` subresource](https://docs.kedify.io/features/scale-adapter/#targets-without-a-scale-subresource), which need a wider grant.

## Usage with an Agones Fleet

First, label the Fleet’s Pod template so the adapter has something to select. Agones does not put a per-fleet label on Pods by default, and the label has to go on the inner Pod template (`spec.template.spec.template`); labels on the GameServer template end up on `GameServer` objects, not on Pods:

```yaml
apiVersion: agones.dev/v1
kind: Fleet
metadata:
  name: game-fleet
spec:
  template:
    spec:
      template:
        metadata:
          labels:
            kedify.io/scale-target: game-fleet
      # ...
```

Create the adapter. Leave `spec.replicas` unset; the controller initializes it from the Fleet, so creating an adapter never changes the Fleet’s replica count:

```yaml
apiVersion: autoscaling.kedify.io/v1alpha1
kind: ScaleAdapter
metadata:
  name: game-fleet
  namespace: default
spec:
  targetRef:
    apiVersion: agones.dev/v1
    kind: Fleet
    name: game-fleet
  selector:
    matchLabels:
      kedify.io/scale-target: game-fleet
```

Then point a regular `ScaledObject` at the adapter instead of the Fleet:

```yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: game-fleet
  namespace: default
spec:
  scaleTargetRef:
    apiVersion: autoscaling.kedify.io/v1alpha1
    kind: ScaleAdapter
    name: game-fleet
  minReplicaCount: 0
  maxReplicaCount: 100
  triggers:
    - type: metrics-api
      metadata:
        # any KEDA or Kedify trigger works
```

Scaling to and from zero is handled by KEDA writing to the adapter’s `/scale` directly (activation bypasses the HPA), which the adapter forwards like any other replica change.

## Targets Without a /scale Subresource

For targets that do not implement the scale contract at all, `spec.desiredReplicasPath` switches the adapter to explicit replica field paths instead of the target’s `/scale` subresource. This makes anything with a numeric “how many” field scalable by KEDA, including resources that represent systems outside the cluster, such as Crossplane managed resources for VM pools or autoscaling groups:

```yaml
apiVersion: autoscaling.kedify.io/v1alpha1
kind: ScaleAdapter
metadata:
  name: worker-pool
  namespace: default
spec:
  targetRef:
    apiVersion: example.org/v1
    kind: Pool
    name: worker-pool
  selector:
    matchLabels:
      kedify.io/scale-target: worker-pool
  desiredReplicasPath: ".spec.size"
  currentReplicasPath: ".status.size" # optional
```

- Paths are simple dot-separated field paths; wildcards and array indices are not supported.

- `currentReplicasPath` is optional. When unset, `status.replicas` mirrors the desired replica count. Pods are deliberately not counted, since field-path targets may have none.

- In this mode the adapter reads and updates the whole target resource instead of its `/scale` subresource, with reads served from an informer cache, so the RBAC grant widens accordingly:

```yaml
agent:
  extraRbacRules:
    - apiGroups: ["example.org"]
      resources: ["pools"]
      verbs: ["get", "list", "watch", "update"]
```

- The HPA still requires a non-empty `status.selector`, so `spec.selector` remains required. For targets without Pods a nominal selector works, and only external metrics with an `AverageValue` target (KEDA’s default) make sense, since pods and resource metrics and `Value` targets count the selected Pods. The `SelectorMatchesPods` condition is not evaluated in this mode.

- Initialization, forwarding, conflict detection, and scaling to and from zero behave exactly as with `/scale` targets.

The flagship field-path target is the `CapacityBuffer` API (`autoscaling.x-k8s.io`) for pre-provisioned warm node capacity: it has `spec.replicas` but no `/scale` subresource. Because it is one Kubernetes API implemented by [Karpenter](https://docs.kedify.io/how-to/karpenter-capacity-buffers/), [Cluster Autoscaler](https://docs.kedify.io/how-to/cluster-autoscaler-capacity-buffers/), and [GKE](https://docs.kedify.io/how-to/gke-capacity-buffers/), the same adapter manifest drives warm capacity on all three; each guide has the complete walkthrough.

## Behavior

- **Initialization.** On the first reconciliation, the controller reads the target’s `/scale`, copies its desired replicas into `spec.replicas` and current replicas into `status.replicas`, and serializes `spec.selector` into `status.selector`.

- **Forwarding.** Whenever `spec.replicas` differs from the target’s desired replicas, the controller updates the target through its `/scale` endpoint, or through `desiredReplicasPath` when configured, never by touching any other field.

- **Mirroring.** The target’s current replicas are continuously mirrored into `status.replicas`, which the HPA uses for per-pod metric math.

- **Single writer.** Exactly one component may manage the target’s desired replicas. Do not combine the adapter with a native autoscaler (for Agones, a `FleetAutoscaler`) on the same target. The adapter is authoritative: if something else changes the target’s desired replicas, the adapter overwrites them, raises the `ConflictingReplicaWriter` condition, and emits a Warning event with reason `ExternalReplicaChange`.

## Conditions

See [ScaleAdapter API reference](https://docs.kedify.io/reference/kubernetes-api/scaleadapter/) for fields and validation.

## Selector Caveat

The HPA validates that `status.selector` is non-empty and parseable before evaluating metrics. For external metrics with an `AverageValue` target (KEDA’s default), that is all the selector is used for. For external metrics with a `Value` target, and for pods and resource metrics, the HPA counts the Pods matched by the selector, so `spec.selector` must actually match the workload’s Pods. The `SelectorMatchesPods` condition flags a mismatch.

## Scope and Limitations

- The target must expose a readable and writable `/scale` subresource, or explicit replica field paths must be configured through `spec.desiredReplicasPath`.

- The target must live in the same namespace as the adapter; cluster-scoped and cross-namespace targets are not supported.

- The adapter never computes desired replicas from metrics; that stays with KEDA and the HPA.

## Continue with this topic

**Diagnose:** [Workload does not scale, or scales too slowly](https://docs.kedify.io/troubleshooting/workload-scaling/).

**Related capabilities:** [Scale custom targets and prewarm node capacity](https://docs.kedify.io/capabilities/node-capacity/).

---
Canonical: https://docs.kedify.io/features/scale-adapter/
Source: src/content/docs/features/scale-adapter.md
Documentation index: https://docs.kedify.io/llms.txt
