Skip to content
Blog

Queue-Driven Agents: Feeding Workloads from Kafka into a LangGraph Fleet

Build queue-driven agent systems with Kafka and LangGraph. Process agent workloads at scale with guaranteed delivery, ordering, and fault tolerance.

Published on September 8, 2026

AI Assistant

What happens when you have 10,000 agent tasks per minute? A single LangGraph instance can’t handle it. You need a queue — and Kafka is the gold standard for high-throughput, fault-tolerant message processing. This guide shows how to connect Kafka to a fleet of LangGraph agents for scalable, reliable task processing.

Why Queue-Driven Architecture

The Problem with Direct Invocation

User Request → LangGraph App → Process → Response
  • Single point of failure
  • No backpressure handling
  • Can’t scale horizontally easily
  • Lost requests on crashes

The Queue-Driven Solution

User Request → Kafka Topic → Consumer Group → LangGraph Workers → Kafka Response Topic
  • Horizontal scaling by adding consumers
  • Guaranteed message delivery
  • Natural backpressure via consumer lag
  • Fault tolerance with replication

Architecture Overview

Producers (API, Webhooks, Batch Jobs)

Kafka Topic: "agent.tasks"

Consumer Group (N workers)
    ├── Worker 1: LangGraph Agent
    ├── Worker 2: LangGraph Agent
    └── Worker N: LangGraph Agent

Kafka Topic: "agent.results"

Results Consumers (API responses, webhooks, storage)

Setting Up Kafka

Docker Compose

version: '3.8'
services:
  zookeeper:
    image: confluentinc/cp-zookeeper:7.4.0
    environment:
      ZOOKEEPER_CLIENT_PORT: 2181
  
  kafka:
    image: confluentinc/cp-kafka:7.4.0
    depends_on:
      - zookeeper
    ports:
      - "9092:9092"
    environment:
      KAFKA_BROKER_ID: 1
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
      KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true"

Create Topics

kafka-topics --create --topic agent.tasks \
  --bootstrap-server localhost:9092 \
  --partitions 6 \
  --replication-factor 1

kafka-topics --create --topic agent.results \
  --bootstrap-server localhost:9092 \
  --partitions 6 \
  --replication-factor 1

Task Producer

from confluent_kafka import Producer
import json
import uuid
from datetime import datetime

class AgentTaskProducer:
    def __init__(self, bootstrap_servers: str = "localhost:9092"):
        self.producer = Producer({
            'bootstrap.servers': bootstrap_servers,
            'acks': 'all',  # Wait for all replicas
            'enable.idempotence': True,  # Exactly-once semantics
        })
    
    def submit_task(
        self,
        task_type: str,
        payload: dict,
        priority: int = 0,
        callback_url: str = None
    ) -> str:
        task_id = str(uuid.uuid4())
        
        task = {
            "task_id": task_id,
            "task_type": task_type,
            "payload": payload,
            "priority": priority,
            "callback_url": callback_url,
            "created_at": datetime.now().isoformat(),
            "retry_count": 0,
            "max_retries": 3,
        }
        
        self.producer.produce(
            topic="agent.tasks",
            key=task_id,
            value=json.dumps(task),
            callback=self._delivery_callback
        )
        self.producer.flush()
        
        return task_id
    
    def _delivery_callback(self, err, msg):
        if err:
            logger.error(f"Task delivery failed: {err}")
        else:
            logger.info(f"Task delivered to {msg.topic()} [{msg.partition()}]")

# Usage
producer = AgentTaskProducer()
task_id = producer.submit_task(
    task_type="summarize_document",
    payload={"document_url": "https://example.com/doc.pdf"},
    priority=1
)

LangGraph Worker

from confluent_kafka import Consumer, KafkaError
import json
import signal

class AgentWorker:
    def __init__(
        self,
        worker_id: str,
        bootstrap_servers: str = "localhost:9092"
    ):
        self.worker_id = worker_id
        self.consumer = Consumer({
            'bootstrap.servers': bootstrap_servers,
            'group.id': f'agent-worker-{worker_id}',
            'auto.offset.reset': 'earliest',
            'enable.auto.commit': False,  # Manual commit for at-least-once
            'max.poll.interval.ms': 300000,  # 5 minutes for long tasks
        })
        
        self.producer = Producer({
            'bootstrap.servers': bootstrap_servers,
        })
        
        self.running = True
        self.agent_graph = self._build_agent_graph()
    
    def _build_agent_graph(self):
        """Build the LangGraph agent for this worker."""
        graph = StateGraph(AgentState)
        # ... add nodes and edges ...
        return graph.compile(checkpointer=checkpointer)
    
    def run(self):
        self.consumer.subscribe(["agent.tasks"])
        
        signal.signal(signal.SIGINT, self._shutdown)
        signal.signal(signal.SIGTERM, self._shutdown)
        
        while self.running:
            msg = self.consumer.poll(1.0)
            
            if msg is None:
                continue
            
            if msg.error():
                if msg.error().code() == KafkaError._PARTITION_EOF:
                    continue
                logger.error(f"Consumer error: {msg.error()}")
                continue
            
            try:
                task = json.loads(msg.value().decode('utf-8'))
                self._process_task(task, msg)
            except Exception as e:
                logger.error(f"Task processing failed: {e}")
                self._handle_failure(task, str(e))
    
    def _process_task(self, task: dict, msg):
        logger.info(f"Processing task {task['task_id']}")
        
        config = {"configurable": {"thread_id": task["task_id"]}}
        
        # Run the agent
        result = self.agent_graph.invoke(
            {
                "task_type": task["task_type"],
                "payload": task["payload"],
                "status": "processing",
            },
            config
        )
        
        # Send result
        self._send_result(task, result)
        
        # Commit offset
        self.consumer.commit(msg)
    
    def _send_result(self, task: dict, result: dict):
        result_message = {
            "task_id": task["task_id"],
            "status": "completed",
            "result": result,
            "worker_id": self.worker_id,
            "completed_at": datetime.now().isoformat(),
        }
        
        self.producer.produce(
            topic="agent.results",
            key=task["task_id"],
            value=json.dumps(result_message)
        )
        self.producer.flush()
        
        # Callback if specified
        if task.get("callback_url"):
            self._send_callback(task["callback_url"], result_message)
    
    def _handle_failure(self, task: dict, error: str):
        task["retry_count"] += 1
        
        if task["retry_count"] <= task["max_retries"]:
            # Requeue with incremented retry count
            self.producer.produce(
                topic="agent.tasks",
                key=task["task_id"],
                value=json.dumps(task)
            )
        else:
            # Send failure result
            self._send_result(task, {
                "status": "failed",
                "error": error,
                "retries_exhausted": True
            })
    
    def _shutdown(self, signum, frame):
        logger.info("Shutting down worker...")
        self.running = False

Running the Fleet

Worker Pool Manager

import multiprocessing
import time

class WorkerFleet:
    def __init__(self, num_workers: int = 4):
        self.num_workers = num_workers
        self.workers = []
    
    def start(self):
        for i in range(self.num_workers):
            worker = AgentWorker(worker_id=str(i))
            process = multiprocessing.Process(target=worker.run)
            process.start()
            self.workers.append(process)
        
        logger.info(f"Started {self.num_workers} workers")
    
    def scale(self, target_workers: int):
        """Scale the fleet up or down."""
        current = len(self.workers)
        
        if target_workers > current:
            # Add workers
            for i in range(current, target_workers):
                worker = AgentWorker(worker_id=str(i))
                process = multiprocessing.Process(target=worker.run)
                process.start()
                self.workers.append(process)
        
        elif target_workers < current:
            # Remove workers
            for i in range(current - target_workers):
                worker = self.workers.pop()
                worker.terminate()
        
        logger.info(f"Scaled from {current} to {target_workers} workers")

# Start fleet
fleet = WorkerFleet(num_workers=4)
fleet.start()

# Auto-scale based on consumer lag
def auto_scale(fleet, lag_threshold=1000):
    while True:
        lag = get_consumer_lag("agent.tasks", "agent-worker")
        
        if lag > lag_threshold * 2:
            fleet.scale(min(fleet.num_workers * 2, 16))
        elif lag < lag_threshold / 2:
            fleet.scale(max(fleet.num_workers // 2, 1))
        
        time.sleep(60)

Monitoring

from prometheus_client import Counter, Gauge, start_http_server

tasks_processed = Counter("tasks_processed_total", "Total tasks processed", ["status"])
consumer_lag = Gauge("consumer_lag", "Consumer lag per partition", ["partition"])
worker_count = Gauge("worker_count", "Number of active workers")

start_http_server(8000)

# In worker loop
tasks_processed.labels(status="completed").inc()
tasks_processed.labels(status="failed").inc()

Best Practices

  • Use manual commits — Commit after successful processing, not before
  • Set max.poll.interval.ms — Account for long-running agent tasks
  • Implement dead letter queues — For tasks that fail repeatedly
  • Monitor consumer lag — Scale workers before lag grows too large
  • Use idempotent producers — Prevent duplicate task submission

Conclusion

Queue-driven agent architectures with Kafka and LangGraph give you horizontal scalability, fault tolerance, and natural backpressure. The pattern is straightforward: produce tasks to Kafka, consume with a fleet of LangGraph workers, and produce results back. Start with manual commits and basic monitoring, add auto-scaling as load grows, and always implement dead letter queues for failed tasks.