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.