Orchestration

Scale Batch Jobs with KEDA Autoscaling

July 1, 2026 Kubezilla Team 16 min read

Batch processing is a cornerstone of modern data pipelines, machine learning inference, and backend operations. From processing daily reports to crunching large datasets, these workloads are often characterized by their episodic nature: they run, complete their task, and then terminate. While Kubernetes excels at managing long-running services, effectively scaling batch jobs to meet fluctuating demand while optimizing resource consumption has historically been a challenge. Traditional Kubernetes Jobs are great for single-shot tasks, but they lack inherent autoscaling capabilities based on external metrics.

Enter KEDA (Kubernetes Event-Driven Autoscaling). KEDA extends Kubernetes to allow event-driven scaling for any container, including Deployments, StatefulSets, and crucially, Jobs. For batch workloads, KEDA introduces the concept of ScaledJob, which dynamically creates and manages Kubernetes Jobs based on the length of a queue, the number of messages in a stream, or various other custom metrics. This means your batch processing infrastructure can scale out instantly when a surge of work arrives and scale back to zero when the queue is empty, leading to significant cost savings and improved efficiency.

This comprehensive guide will walk you through setting up KEDA and leveraging ScaledJobs to intelligently autoscale your batch workloads. We’ll cover everything from installation and configuration to practical examples and best practices, ensuring your Kubernetes clusters are humming with efficient, event-driven batch processing. Get ready to transform how you manage episodic tasks in Kubernetes!

TL;DR: Autoscaling Batch Jobs with KEDA ScaledJobs

KEDA’s ScaledJob resource enables event-driven autoscaling for Kubernetes batch workloads, dynamically creating Jobs based on external metrics like queue length. This optimizes resource usage and costs.

  • Install KEDA: helm repo add kedacore https://kedacore.github.io/charts && helm install keda kedacore/keda --namespace keda --create-namespace
  • Define a ScaledJob: Create a YAML manifest including Job spec and KEDA triggers (e.g., SQS, Kafka).
  • Example ScaledJob (SQS):
  • 
    apiVersion: keda.sh/v1alpha1
    kind: ScaledJob
    metadata:
      name: my-sqs-scaledjob
    spec:
      jobTargetRef:
        template:
          spec:
            containers:
            - name: worker
              image: my-batch-processor:latest
              env:
              - name: SQS_QUEUE_NAME
                value: my-queue
            restartPolicy: Never
        backoffLimit: 4
      pollingInterval: 30 # Optional. Default: 30 seconds
      successfulJobsHistoryLimit: 5 # Optional. Default: 10
      failedJobsHistoryLimit: 5   # Optional. Default: 10
      maxReplicaCount: 10 # Optional. Default: 100
      triggers:
      - type: aws-sqs-queue
        metadata:
          queueURL: https://sqs.us-east-1.amazonaws.com/123456789012/my-queue
          queueLength: "5" # Scale a job for every 5 messages
          awsRegion: "us-east-1"
          identityOwner: "pod" # Use IRSA for authentication
    
  • Monitor: Check KEDA logs, Kubernetes Jobs, and HPA status.
  • Cleanup: kubectl delete scaledjob my-sqs-scaledjob && helm uninstall keda --namespace keda

Prerequisites

Before we dive in, ensure you have the following tools and knowledge:

  • Kubernetes Cluster: A running Kubernetes cluster (v1.16+). This could be Minikube, Kind, or a cloud-managed cluster (EKS, AKS, GKE).
  • kubectl: The Kubernetes command-line tool, configured to connect to your cluster. You can find installation instructions on the official Kubernetes documentation.
  • Helm: The Kubernetes package manager (v3+). Follow the Helm installation guide if you don’t have it.
  • Basic Kubernetes Concepts: Familiarity with Pods, Deployments, Jobs, and Services.
  • Access to a Message Queue (Optional but Recommended): While KEDA supports many triggers, a message queue like AWS SQS, Azure Service Bus, or Apache Kafka is ideal for demonstrating ScaledJob. For this guide, we’ll use AWS SQS as an example.
  • Cloud Provider Credentials (if using cloud-specific triggers): For AWS SQS, you’ll need AWS credentials configured or, preferably, IAM Roles for Service Accounts (IRSA) setup on your EKS cluster. Similar mechanisms exist for Azure and GCP.

Step-by-Step Guide: Autoscaling Batch Workloads with KEDA ScaledJobs

Step 1: Install KEDA

KEDA is installed as a set of custom resource definitions (CRDs) and a controller within your Kubernetes cluster. The easiest and most recommended way to install KEDA is by using Helm. This will set up the KEDA operator and all necessary components to start scaling your applications.

First, add the KEDA Helm repository. Then, you can install KEDA into its own namespace, typically named keda. This ensures a clean separation of KEDA components from your application workloads. KEDA’s architecture is designed to be lightweight, extending the control plane rather than modifying your application containers, making it a powerful yet non-intrusive solution for autoscaling.


# Add the KEDA Helm repository
helm repo add kedacore https://kedacore.github.io/charts

# Update your Helm repositories
helm repo update

# Install KEDA into the 'keda' namespace
helm install keda kedacore/keda --namespace keda --create-namespace

Verify KEDA Installation

After installation, verify that the KEDA pods are running and healthy. You should see pods for the KEDA operator and the metrics server. The metrics server is crucial as it exposes the custom metrics that KEDA collects to the Horizontal Pod Autoscaler (HPA) and, indirectly, to ScaledJobs.


kubectl get pods --namespace keda

# Expected Output:
# NAME                                  READY   STATUS    RESTARTS   AGE
# keda-operator-56c87d46c4-abcde        1/1     Running   0          2m
# keda-operator-metrics-apiserver-7f65f745d-fghij   1/1     Running   0          2m

kubectl get crd | grep keda.sh

# Expected Output:
# clusteredscaledobjects.keda.sh             2023-10-27T10:00:00Z
# scaledjobs.keda.sh                         2023-10-27T10:00:00Z
# scaledobjects.keda.sh                      2023-10-27T10:00:00Z

The presence of scaledjobs.keda.sh in the CRD list confirms that KEDA is ready to manage your batch workloads.

Step 2: Prepare Your Batch Worker Application

For ScaledJobs, we need a container image that represents our batch processing logic. This application should be designed to process a single unit of work (e.g., one message from a queue) and then terminate gracefully. A common pattern is for the worker to read from a message queue, process the data, and then exit. The ScaledJob will then create new instances of this worker as needed.

Let’s create a simple Python worker that simulates processing messages from an SQS queue. This worker will print a message indicating it’s processing, simulate some work, and then exit. In a real-world scenario, it would interact with the SQS API to receive and delete messages.

Create a Python Worker Script (worker.py)

This script is simplified. A production worker would typically use boto3 to interact with SQS, receive messages, process them, and then delete them. For this example, we’ll just simulate the work.


# worker.py
import os
import time
import random

if __name__ == "__main__":
    job_id = os.getenv("KUBERNETES_POD_NAME", "unknown-pod")
    queue_name = os.getenv("SQS_QUEUE_NAME", "default-queue")
    print(f"[{job_id}] Starting batch worker for queue: {queue_name}")
    
    # Simulate processing a message
    processing_time = random.uniform(5, 15)
    print(f"[{job_id}] Simulating work for {processing_time:.2f} seconds...")
    time.sleep(processing_time)
    
    print(f"[{job_id}] Work finished. Exiting.")

Create a Dockerfile

This Dockerfile will build an image containing our Python worker.


# Dockerfile
FROM python:3.9-slim-buster

WORKDIR /app

COPY worker.py .

CMD ["python", "worker.py"]

Build and Push the Docker Image

Replace your-dockerhub-username with your Docker Hub username or the path to your container registry. If you’re using a private registry, ensure your Kubernetes cluster has credentials to pull from it.


docker build -t your-dockerhub-username/my-batch-processor:latest .
docker push your-dockerhub-username/my-batch-processor:latest

For more advanced container image security, consider integrating tools like Sigstore and Kyverno to sign and verify your images.

Step 3: Configure AWS SQS (or your chosen message queue)

For this example, we’ll use AWS SQS. You’ll need an SQS queue to act as the event source for KEDA. Create a standard SQS queue in your AWS account.

Create an SQS Queue

You can create an SQS queue via the AWS Management Console, AWS CLI, or CloudFormation/Terraform. For simplicity, we’ll assume you create it manually. Note down its URL and region.

Example SQS Queue URL: https://sqs.us-east-1.amazonaws.com/123456789012/my-keda-queue

Configure IAM Permissions for KEDA

KEDA needs permissions to read metrics from your SQS queue. The most secure way to do this in Kubernetes is by using IAM Roles for Service Accounts (IRSA) if you’re on EKS. If not, you might need to provide AWS credentials as a Kubernetes secret.

Option A: IAM Roles for Service Accounts (IRSA – Recommended for EKS)

1. Create an IAM policy that allows KEDA to read SQS queue attributes:


{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "sqs:GetQueueAttributes",
                "sqs:ListQueues"
            ],
            "Resource": "arn:aws:sqs:REGION:ACCOUNT_ID:my-keda-queue"
        }
    ]
}

2. Create an IAM role and attach this policy. Configure a trust policy for the role to allow your Kubernetes service account to assume it. This involves specifying your OIDC provider URL.

3. Annotate the Kubernetes Service Account that your ScaledJob will use with the ARN of the IAM role. KEDA will automatically use this. We’ll define this Service Account in the next step.

Option B: Kubernetes Secret (Less Recommended for Production)

If IRSA isn’t an option, you can create a Kubernetes secret with your AWS access key and secret key. This secret would then be referenced in your ScaledJob definition. This method is generally less secure than IRSA.


# Replace with your actual AWS credentials
kubectl create secret generic aws-credentials \
  --from-literal=AWS_ACCESS_KEY_ID='YOUR_AWS_ACCESS_KEY_ID' \
  --from-literal=AWS_SECRET_ACCESS_KEY='YOUR_AWS_SECRET_ACCESS_KEY' \
  --namespace default # Or your application namespace

For this guide, we’ll demonstrate using IRSA, as it’s the best practice for cloud environments.

Step 4: Define the ScaledJob

Now, let’s create the ScaledJob resource. This YAML manifest combines a Kubernetes Job specification with KEDA-specific scaling triggers. The ScaledJob defines how your batch worker should run and when KEDA should create new instances of it.

Key components of a ScaledJob:

  • jobTargetRef: This section is almost identical to a standard Kubernetes Job spec, defining the pod template, restart policy (usually Never for batch jobs), and other job-specific settings like backoffLimit.
  • pollingInterval: How often KEDA should check the scaler (e.g., SQS queue) for new events.
  • successfulJobsHistoryLimit / failedJobsHistoryLimit: How many completed/failed jobs to retain in Kubernetes history.
  • maxReplicaCount: The maximum number of concurrent Jobs KEDA can create. This acts as a safeguard to prevent resource exhaustion. Consider using Karpenter for efficient node autoscaling to support these bursty workloads.
  • triggers: This is where you define the event source. KEDA supports a vast array of triggers, from message queues (SQS, Kafka, RabbitMQ) to databases, Prometheus metrics, and custom external scalers. Each trigger type has its own metadata for configuration.

Here’s a ScaledJob definition for our SQS worker, using IRSA for authentication:


# scaledjob-sqs.yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledJob
metadata:
  name: sqs-message-processor
  namespace: default # Or your application namespace
spec:
  jobTargetRef:
    template:
      spec:
        serviceAccountName: keda-sqs-sa # Service Account with IRSA annotations
        containers:
        - name: worker
          image: your-dockerhub-username/my-batch-processor:latest # Your image
          imagePullPolicy: IfNotPresent
          env:
          - name: SQS_QUEUE_NAME
            value: my-keda-queue # Your SQS queue name
        restartPolicy: Never # Crucial for batch jobs
    backoffLimit: 4 # Number of retries before marking job as failed
  pollingInterval: 10 # Check SQS every 10 seconds
  successfulJobsHistoryLimit: 5 # Retain 5 successful jobs
  failedJobsHistoryLimit: 5   # Retain 5 failed jobs
  maxReplicaCount: 10 # Max 10 concurrent jobs
  triggers:
  - type: aws-sqs-queue
    metadata:
      queueURL: https://sqs.us-east-1.amazonaws.com/123456789012/my-keda-queue # Your SQS Queue URL
      queueLength: "1" # Create one job for every 1 message in the queue
      awsRegion: "us-east-1" # Your AWS Region
      identityOwner: "pod" # Use IRSA for authentication
      # If not using IRSA, you'd use:
      # awsAccessKeyIDFromEnv: AWS_ACCESS_KEY_ID
      # awsSecretAccessKeyFromEnv: AWS_SECRET_ACCESS_KEY
      # OR reference a secret:
      # awsAccessKeyId:
      #   valueFrom:
      #     secretKeyRef:
      #       name: aws-credentials
      #       key: AWS_ACCESS_KEY_ID
      # awsSecretAccessKey:
      #   valueFrom:
      #     secretKeyRef:
      #       name: aws-credentials
      #       key: AWS_SECRET_ACCESS_KEY
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: keda-sqs-sa
  namespace: default
  annotations:
    # Replace with your actual OIDC provider and IAM Role ARN
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/keda-sqs-processor-role

Important: Replace placeholders like your-dockerhub-username, my-keda-queue, 123456789012 (AWS Account ID), and us-east-1 with your specific values. Ensure the IAM Role ARN in the Service Account annotation is correct and has the necessary SQS permissions.


kubectl apply -f scaledjob-sqs.yaml

Verify ScaledJob Creation

Check if the ScaledJob and Service Account are created successfully.


kubectl get scaledjob sqs-message-processor
kubectl get sa keda-sqs-sa

# Expected Output:
# NAME                      AGE
# sqs-message-processor     10s

# NAME          SECRETS   AGE
# keda-sqs-sa   0         10s

Step 5: Test Autoscaling

Now that our ScaledJob is defined, let’s trigger it by sending messages to our SQS queue. KEDA will detect the messages and start creating Kubernetes Jobs.

Send Messages to SQS

You can use the AWS CLI to send messages to your SQS queue. Send multiple messages to observe scaling behavior.


# Replace with your SQS queue URL
aws sqs send-message --queue-url https://sqs.us-east-1.amazonaws.com/123456789012/my-keda-queue --message-body "message-1"
aws sqs send-message --queue-url https://sqs.us-east-1.amazonaws.com/123456789012/my-keda-queue --message-body "message-2"
aws sqs send-message --queue-url https://sqs.us-east-1.amazonaws.com/123456789012/my-keda-queue --message-body "message-3"
aws sqs send-message --queue-url https://sqs.us-east-1.amazonaws.com/123456789012/my-keda-queue --message-body "message-4"
aws sqs send-message --queue-url https://sqs.us-east-1.amazonaws.com/123456789012/my-keda-queue --message-body "message-5"

Observe KEDA Creating Jobs

Monitor the Kubernetes Jobs in your namespace. You should see new jobs being created as KEDA detects messages in the SQS queue.


kubectl get jobs --watch

# Expected Output (example, job names will vary):
# NAME                            COMPLETIONS   DURATION   AGE
# sqs-message-processor-abcde     0/1           0s         0s
# sqs-message-processor-fghij     0/1           0s         0s
# sqs-message-processor-klmno     0/1           0s         0s
# ... (more jobs as messages are processed)
# sqs-message-processor-abcde     1/1           12s        12s
# sqs-message-processor-fghij     1/1           10s        10s
# sqs-message-processor-klmno     1/1           15s        15s

You can also check the logs of the KEDA operator to see its scaling decisions:


kubectl logs -f deployment/keda-operator -n keda

The KEDA operator logs will show messages indicating it’s polling the SQS queue and creating/deleting jobs based on the queue length.

Check Pod Logs

Each job will create a pod. You can check the logs of these pods to see your worker script in action.


# Get a list of pods associated with your ScaledJob
kubectl get pods -l keda.sh/scaledjob-name=sqs-message-processor

# Get logs from one of the pods (replace with an actual pod name)
kubectl logs sqs-message-processor-abcde-xyz

You should see output similar to:


# Expected Output (from pod logs):
# [sqs-message-processor-abcde-xyz] Starting batch worker for queue: my-keda-queue
# [sqs-message-processor-abcde-xyz] Simulating work for 8.45 seconds...
# [sqs-message-processor-abcde-xyz] Work finished. Exiting.

Once all messages are processed and the queue is empty (and KEDA has polled it a few times), KEDA will stop creating new jobs, and the existing job pods will terminate after completing their work. The ScaledJob itself remains, ready to scale again when new events arrive.

Production Considerations

Deploying ScaledJobs in a production environment requires careful planning beyond just the basic setup:

  1. Resource Requests and Limits: Define appropriate CPU and memory requests and limits for your batch worker containers. This is crucial for scheduler efficiency and preventing resource exhaustion on nodes. Without them, your pods might get evicted or starve other critical services.
  2. Node Autoscaling: For bursty batch workloads, your cluster might need to scale its underlying nodes. Integrate with a node autoscaler like Karpenter or the Cluster Autoscaler. Karpenter is particularly effective for cost optimization as it can provision just-in-time nodes tailored to your workload’s specific requirements.
  3. Error Handling and Dead-Letter Queues (DLQs): Your batch worker must be resilient. Implement robust error handling, retries, and integrate with a Dead-Letter Queue (DLQ) for messages that repeatedly fail processing. This prevents poison pill messages from blocking your queue and ensures no data is lost.
  4. Monitoring and Alerting: Monitor KEDA’s health, the number of active Jobs, queue lengths, and the success/failure rate of your batch workers. Integrate with your existing observability stack. Tools like eBPF Observability with Hubble can provide deep insights into network and application performance. Prometheus and Grafana are excellent choices for metrics and dashboards.
  5. Security:
    • IAM Roles for Service Accounts (IRSA): Always use IRSA (or equivalent for Azure/GCP) for cloud-provider authentication. Avoid embedding credentials directly in secrets if possible.
    • Network Policies: Restrict network access for your batch workers using Kubernetes Network Policies. They should only be able to communicate with necessary services (e.g., message queue, database). Consider using CNI plugins like Cilium for advanced networking features, including WireGuard encryption for Pod-to-Pod traffic.
    • Image Security: Scan your container images for vulnerabilities and use trusted base images. Implement image signing and verification with tools like Sigstore, possibly enforced by admission controllers like Kyverno.
  6. Job History Limits: Carefully set successfulJobsHistoryLimit and failedJobsHistoryLimit to balance diagnostic information with etcd storage overhead. Too many completed jobs can bloat your etcd database.
  7. Concurrency and Throttling: Be mindful of the maxReplicaCount. Ensure your backend services (databases, APIs) can handle the increased load when many batch workers are active simultaneously. Implement rate limiting or circuit breakers within your workers if necessary.
  8. Graceful Shutdown: Design your worker application to handle Kubernetes termination signals (SIGTERM) gracefully, allowing it to finish processing its current message before exiting.
  9. Testing: Thoroughly test your ScaledJob under various load conditions, including peak load and zero load, to ensure it scales correctly and efficiently.

Troubleshooting

Here are some common issues you might encounter with KEDA ScaledJobs and their solutions:

  1. Issue: KEDA is installed, but no Jobs are being created when messages are in the queue.

    Solution:

    • Check KEDA Operator Logs: The first place to look is the KEDA operator logs for errors related to polling the scaler or creating jobs.
      
      kubectl logs -f deployment/keda-operator -n keda
      
    • Verify ScaledJob Status: Check the status of your ScaledJob. It might contain useful warnings or error messages.
      
      kubectl describe scaledjob sqs-message-processor
      
    • Authentication Issues: Often, the problem is that KEDA cannot authenticate with the external system (e.g., AWS SQS).
      • If using IRSA, double-check the Service Account annotations and the IAM role’s trust policy and permissions. Ensure the OIDC provider is correctly configured for your cluster.
      • If using secrets, verify the secret exists in the correct namespace and the keys (AWS_ACCESS_KEY_ID, etc.) match those expected by the KEDA trigger.
    • Network Connectivity: Ensure KEDA (and your worker pods) can reach the external message queue. Check network policies if you have them enabled (refer to Kubernetes Network Policies).
    • Scaler Configuration: Review the metadata section of your trigger in the ScaledJob. A typo in queueURL, awsRegion, or queueLength can prevent scaling.
  2. Issue: Jobs are created, but they immediately fail or get stuck.

    Solution:

    • Check Pod Logs: Get the logs of the failing job pods. This will almost always reveal the application-level error.
      
      kubectl get pods -l keda.sh/scaledjob-name=sqs-message-processor
      kubectl logs <failing-pod-name>
      
    • Image Pull Issues: Ensure your container image can be pulled. Check imagePullSecrets if using a private registry, and verify the image name/tag.
      
      kubectl describe pod <failing-pod-name> | grep -i 'imagepull'
      
    • Worker Application Errors: Your worker application might have bugs, incorrect environment variables, or missing dependencies. Test it locally if possible.
    • Resource Constraints: The container might be OOMKilled (Out Of Memory Killed) or starved of CPU. Check pod events and adjust resource requests/limits.
  3. Issue: KEDA creates too many or too few Jobs.

    Solution:

    • queueLength/Target Value: Adjust the queueLength (or equivalent for other scalers) in your trigger metadata. If queueLength: "1" means one job per message, and you have 100 messages, it will create 100 jobs (up to maxReplicaCount). If you want fewer jobs, increase the target value (e.g., queueLength: "10" for one job per 10 messages).
    • maxReplicaCount: Ensure maxReplicaCount in your ScaledJob is set appropriately. If it’s too low, KEDA won’t scale out further. If it’s too high, you might overwhelm your cluster or backend services.
    • pollingInterval: A very long polling interval might cause slow scaling reactions. A very short one might put unnecessary load on the scaler. Balance this based on your latency requirements.
  4. Issue: Completed Jobs are piling up in the cluster.

    Solution:

    • Adjust History Limits: Use successfulJobsHistoryLimit and failedJobsHistoryLimit in your ScaledJob spec. These values control how many completed/failed jobs Kubernetes retains. Set them to a reasonable number (e.g., 5-10) to keep your cluster clean while retaining enough history for debugging.
      
      spec:
        successfulJobsHistoryLimit: 5
        failedJobsHistoryLimit: 5
      
  5. Issue: KEDA is not scaling down to zero.

    Solution:

    • Queue Empty Check: Ensure your worker correctly processes and deletes messages from the queue. If messages are not deleted, KEDA will continuously see messages and try to scale.
    • pollingInterval: It takes at least one pollingInterval after the queue is empty for KEDA to detect zero messages and stop creating jobs.
    • cooldownPeriod (for ScaledObject, not directly ScaledJob): While ScaledJob doesn’t have a direct cooldownPeriod like ScaledObject, the overall scaling down behavior is influenced by polling. Ensure your queue is truly empty and KEDA’s logs confirm it sees zero messages.

FAQ Section

  1. What is the difference between ScaledJob and ScaledObject?

    ScaledObject is used for autoscaling long-running workloads like Deployments and StatefulSets. It adjusts the number of replicas of these resources. ScaledJob, on the other hand, is specifically designed for batch workloads. It dynamically creates and manages Kubernetes Jobs based on event sources, where each job typically processes a unit of work and then terminates. Think of ScaledObject for services and ScaledJob for tasks.

  2. Can I use any Kubernetes Job spec within a ScaledJob?

    Yes, the jobTargetRef.template section of a ScaledJob is essentially a standard Kubernetes PodTemplateSpec from a Job. This means you can leverage all the features of a regular Kubernetes Job, including init containers, sidecars, volumes, resource requests/limits, and more. The only strict requirement for batch processing is typically restartPolicy: Never.

  3. How does KEDA handle authentication to external systems like AWS SQS or Azure Service Bus?

    KEDA offers several methods for authentication, prioritizing secure cloud-native approaches:

    • Pod Identity: Recommended for cloud providers (e.g., AWS IRSA, Azure AD Workload Identity, GCP Workload Identity). This allows a Kubernetes Service Account to assume an IAM role/identity.
    • Kubernetes Secrets: Credentials (e.g., API keys, connection strings) can be stored in Kubernetes secrets and referenced by the KEDA trigger.
    • Environment Variables: Less secure, but possible.

    Always prefer pod identity where available for

Leave a comment