Autoscaling

Guidedraft
On this page

Relay computes how many executors each application version needs from queue backlog, so autoscalers like KEDA can size deployments per version, drain old versions to zero, and drive rollouts. Relay recommends counts; it never scales anything itself. See ADR 0012 for the design.

This follows the upstream contract (upstream autoscaling docs, confirmed 2026-09-21).

Attaching a policy

A policy names the queue whose backlog drives the executor count:

curl -X PUT "$RELAY/v2/orgs/my-org/apps/my-app/autoscaling-policy" \
  -H "Authorization: Bearer $RELAY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"queue": "orders"}'

Relay validates the queue against a running executor before storing the policy. The queue must exist and must have a worker concurrency set; otherwise the write is rejected with 400. Relay also rejects partitioned queues for now, although the 2026-09-30 contract no longer forbids them (recorded in REST surface). The optional rollout section caps old versions during rollouts:

{
  "queue": "orders",
  "rollout": {
    "maxOldApplicationVersions": 2,
    "maxExecutorsForOldApplicationVersions": 1
  }
}

GET the same path reads the stored policy (404 when none is set); DELETE turns autoscaling off (204 even when no policy was stored; the audit entry records whether one was removed). Setting and deleting require application.write.

Reading recommendations

Per version (for a KEDA scaler polling one deployment):

curl -H "Authorization: Bearer $RELAY_API_KEY" \
  "$RELAY/v2/orgs/my-org/apps/my-app/autoscale/versions/latest"

All versions at once (for an operator owning every deployment):

curl -H "Authorization: Bearer $RELAY_API_KEY" \
  "$RELAY/v2/orgs/my-org/apps/my-app/autoscale"

Each entry reports applicationVersion, isLatest, desiredExecutors, queueName, queueDepth, and observedAt.

How the count is computed

Backlog is the ENQUEUED plus PENDING depth on the policy queue, counted per application version from executor-reported listings:

desiredExecutors = ceil(queueDepth / workerConcurrency)

A queue-level concurrency limit additionally caps the recommendation at ceil(concurrency / workerConcurrency). The latest version is always reported with at least one executor; an old version with no remaining work is omitted, which signals that its deployment can be deleted. maxExecutorsForOldApplicationVersions caps every old version, and at most maxOldApplicationVersions old versions are listed (default 0: latest only). Old versions are ordered most recently connected first. The latest version resolves from the recorded latest version when set, otherwise from the most recently connected executor, otherwise from the lexicographic maximum of known versions. When no version is known at all (no recorded latest, no executors, no backlog), the all-versions endpoint returns an empty list rather than inventing a version; the single-version endpoint still honors an explicitly recorded latest at its minimum of one.

KEDA example

One ScaledObject per version deployment, mapping the absolute count one-to-one to replicas:

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: my-app-latest
spec:
  scaleTargetRef:
    name: my-app-v2
  minReplicaCount: 1
  maxReplicaCount: 32
  triggers:
    - type: metrics-api
      metadata:
        url: "https://relay.example.com/v2/orgs/my-org/apps/my-app/autoscale/versions/latest"
        valueLocation: "desiredExecutors"
        authMode: "bearer"

Errors

Status Meaning
400 The policy names no queue, an unknown or partitioned queue, a queue without worker concurrency, or a negative rollout cap.
404 No autoscaling policy is stored, or the requested version was never registered.
502 / 503 No healthy executor answered the queue or backlog query.