KubeMQ
IntegrationsKEDAHow-to guides

Autoscale a Queue Consumer

Configure a KEDA ScaledObject to scale a KubeMQ queue consumer Deployment up and down based on the Waiting message count.

The KubeMQ KEDA external scaler reads the Waiting message count from a KubeMQ queue channel and exposes it as a KEDA metric. KEDA's Horizontal Pod Autoscaler (HPA) then drives a queue-consumer Deployment between a minimum and maximum number of replicas, targeting roughly one replica per targetWaiting pending messages. The goal of this guide is to wire up that loop: as backlog grows the consumer scales out, and as the queue drains it scales back in.

Before You Start

This guide assumes the scaler is already deployed and you know its Service FQDN. The format is <service-name>.<namespace>.svc.cluster.local:9090 — for a default Helm install that is kubemq-keda-scaler.default.svc.cluster.local:9090. You also need a KubeMQ broker reachable from the cluster (for example kubemq.default.svc.cluster.local:50000) and a Deployment that consumes from a queue channel.

Basic ScaledObject

Create a ScaledObject that targets your consumer Deployment and points one external trigger at the scaler. KEDA calls the scaler every pollingInterval seconds, scales toward targetWaiting messages per replica, and waits cooldownPeriod seconds of inactivity before scaling back down to minReplicaCount.

scaled-object-basic.yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: kubemq-queue-scaler
spec:
  scaleTargetRef:
    name: my-queue-consumer
  pollingInterval: 15
  cooldownPeriod: 60
  minReplicaCount: 1
  maxReplicaCount: 10
  triggers:
    - type: external
      metadata:
        scalerAddress: kubemq-keda-scaler.default.svc.cluster.local:9090
        kubemqAddress: kubemq.default.svc.cluster.local:50000
        queueName: my-queue
        targetWaiting: "10"
Apply it
kubectl apply -f scaled-object-basic.yaml

With these settings KEDA keeps at least one replica running at all times, scales out to a ceiling of ten, polls the queue depth every 15 seconds, and tolerates a 60-second cooldown before scaling in.

Trigger Metadata

Each external trigger carries the metadata the scaler needs to find the broker and the queue. KEDA passes this map straight through to the scaler's ParseScalerMetadata, so the field names must match exactly.

FieldRequiredDefaultDescription
scalerAddressYesThe scaler Service FQDN on port 9090 (this is the KEDA-side address, not a trigger metadata field validated by the scaler).
kubemqAddressYesKubeMQ broker address as host:port.
queueNameYesThe queue channel name to monitor.
targetWaitingNo10Target Waiting messages per replica.

The scalerAddress tells KEDA which gRPC endpoint to call; the remaining fields are forwarded to the scaler, which dials kubemqAddress, calls ListQueuesChannels for queueName, and reads the channel's outgoing Waiting count. targetWaiting becomes the HPA target value for the kubemq-queue-waiting metric.

How the Math Works

KEDA's HPA divides the current backlog by the per-replica target and rounds up, then clamps the result to your replica bounds:

Desired replica calculation
desiredReplicas = clamp(
  ceil(currentWaiting / targetWaiting),
  minReplicaCount,
  maxReplicaCount,
)

For example, with targetWaiting: "10" and 45 messages waiting, the HPA wants ceil(45 / 10) = 5 replicas. A lower targetWaiting makes scaling more aggressive — each replica is responsible for fewer messages, so the same backlog produces more pods. A higher value packs more work onto each replica and scales more conservatively.

Activation and the Minimum Floor

Computing a replica count and deciding whether the workload should be active at all are two separate decisions. The scaler's IsActive RPC returns active only when the backlog exceeds activationTargetWaiting:

scaler/scaler.go — IsActive
return &pb.IsActiveResponse{
	Result: float64(waiting) > meta.ActivationTargetWaiting,
}, nil

activationTargetWaiting defaults to 0, so with the basic configuration any single pending message keeps the trigger active. Because minReplicaCount is 1 here, the workload never drops below one replica regardless of activation — the floor is held by minReplicaCount, while activationTargetWaiting becomes meaningful only when you allow the workload to reach zero (see scale-to-zero below).

Higher-Throughput Variant: ML Inference

For bursty, latency-sensitive workloads you can poll faster, scale to a higher ceiling, and add a fallback so the consumer keeps running even if the scaler is briefly unreachable. This example scales GPU inference workers from zero to twenty based on the inference request queue depth.

scaled-object-ml-inference.yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: ml-inference-scaler
spec:
  scaleTargetRef:
    name: gpu-inference-worker
  pollingInterval: 5
  cooldownPeriod: 300
  minReplicaCount: 0
  maxReplicaCount: 20
  fallback:
    failureThreshold: 3
    replicas: 2
  triggers:
    - type: external
      metadata:
        scalerAddress: kubemq-keda-scaler.default.svc.cluster.local:9090
        kubemqAddress: kubemq.default.svc.cluster.local:50000
        queueName: inference-requests
        targetWaiting: "5"
        activationTargetWaiting: "1"

Compared to the basic object, this variant:

  • Polls every 5 seconds for faster reaction to incoming requests.
  • Sets minReplicaCount: 0 so idle GPU workers cost nothing.
  • Uses targetWaiting: "5" for a tighter per-replica budget.
  • Sets activationTargetWaiting: "1". The activation check is strictly greater-than (Waiting > 1), so the workload only activates once at least two requests are waiting (Waiting >= 2); a backlog of exactly 1 does not wake it.
  • Adds fallback, so if the scaler returns errors for 3 consecutive polls, the HPA holds the Deployment at 2 replicas rather than guessing.

The scaler returns a gRPC error (not a fake Waiting=0) when the KubeMQ broker is unreachable, unauthenticated, throttled, or times out. That error is what lets KEDA's fallback block engage. Without fallback, an unreachable scaler leaves the Deployment at its current replica count.

ScaledJob Variant: One Job per Batch

When each message represents an independent unit of work, a ScaledJob is often a better fit than a ScaledObject: KEDA creates Kubernetes Jobs on demand instead of keeping long-lived pods. With targetWaiting: "1" the scaler effectively requests one Job per waiting message, up to maxReplicaCount.

scaled-job.yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledJob
metadata:
  name: kubemq-batch-job
spec:
  jobTargetRef:
    template:
      spec:
        containers:
          - name: worker
            image: my-batch-worker:latest
            env:
              - name: KUBEMQ_ADDRESS
                value: kubemq.default.svc.cluster.local:50000
              - name: QUEUE_NAME
                value: batch-queue
        restartPolicy: Never
    backoffLimit: 3
  pollingInterval: 10
  maxReplicaCount: 50
  successfulJobsHistoryLimit: 5
  failedJobsHistoryLimit: 3
  triggers:
    - type: external
      metadata:
        scalerAddress: kubemq-keda-scaler.default.svc.cluster.local:9090
        kubemqAddress: kubemq.default.svc.cluster.local:50000
        queueName: batch-queue
        targetWaiting: "1"

Note that a ScaledJob uses jobTargetRef (a Job template) rather than scaleTargetRef, and the worker container reads the queue address and name from environment variables so each Job knows which channel to drain.

Verify Scaling

With a ScaledObject applied, push messages onto the queue and watch the consumer react.

Send messages to the queue so the Waiting count climbs above targetWaiting.

Confirm the ScaledObject is ready and check the HPA the operator created for it.

Inspect the ScaledObject and its HPA
kubectl get scaledobject kubemq-queue-scaler
kubectl get hpa

Watch the target Deployment scale out as the backlog grows.

Watch replicas
kubectl get deploy my-queue-consumer --watch

Drain the queue. After the backlog clears and cooldownPeriod (60 seconds in the basic example) elapses with no activity, the Deployment scales back down to minReplicaCount.

Validation Rules

The scaler parses targetWaiting and activationTargetWaiting as 64-bit floats and rejects values that are not finite. targetWaiting must be a finite, strictly positive number; activationTargetWaiting must be a finite, non-negative number.

scaler/config.go — validation
if parsed <= 0 || math.IsNaN(parsed) || math.IsInf(parsed, 0) {
	return nil, status.Error(codes.InvalidArgument, "targetWaiting must be a finite positive number")
}
// ...
if parsed < 0 || math.IsNaN(parsed) || math.IsInf(parsed, 0) {
	return nil, status.Error(codes.InvalidArgument, "activationTargetWaiting must be a finite non-negative number")
}

A value such as targetWaiting: "0", a negative number, or a non-numeric string causes the scaler to return an InvalidArgument gRPC status. KEDA surfaces this as an error on the ScaledObject and logs it — scaling for that trigger will not start until the metadata is corrected. Always quote numeric metadata values ("10", "5", "1") since trigger metadata is a string map.

Was this page helpful?

On this page