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.
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"kubectl apply -f scaled-object-basic.yamlWith 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.
| Field | Required | Default | Description |
|---|---|---|---|
scalerAddress | Yes | — | The scaler Service FQDN on port 9090 (this is the KEDA-side address, not a trigger metadata field validated by the scaler). |
kubemqAddress | Yes | — | KubeMQ broker address as host:port. |
queueName | Yes | — | The queue channel name to monitor. |
targetWaiting | No | 10 | Target 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:
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:
return &pb.IsActiveResponse{
Result: float64(waiting) > meta.ActivationTargetWaiting,
}, nilactivationTargetWaiting 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.
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: 0so 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 exactly1does 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.
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.
kubectl get scaledobject kubemq-queue-scaler
kubectl get hpaWatch the target Deployment scale out as the backlog grows.
kubectl get deploy my-queue-consumer --watchDrain 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.
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.
Related
- Scale to Zero to let idle consumers drop to no replicas with push-based activation.
- TLS and Authentication to connect the scaler to a secured KubeMQ broker.
- ScaledObject metadata for the full trigger metadata table.
- Error codes for the KubeMQ-error-to-gRPC mapping and KEDA
fallbackbehavior. - Queues for the core queue concept the
Waitingcount comes from.
Was this page helpful?
Getting Started with KEDA Autoscaling
Install the KubeMQ KEDA scaler with Helm and autoscale your first queue consumer on queue depth in minutes.
Scale to Zero with Push Mode
Use the external-push trigger to scale a KubeMQ queue consumer from zero replicas and back, with fast scale-from-zero detection.