Advanced

Event-Driven AI Pattern

Event-driven architecture decouples AI processing from user requests, enabling systems that react to events in real-time, scale independently, and handle long-running AI tasks without blocking. Combined with streaming, it creates responsive AI experiences that feel instant even when processing takes seconds.

Why Event-Driven AI?

Traditional request-response architectures have limitations for AI workloads:

  • AI is slow: LLM calls take 2-30 seconds. Holding an HTTP connection open that long creates timeout risks and poor user experience.
  • AI is unpredictable: Processing time varies wildly based on input size, model load, and task complexity. Fixed timeouts do not work well.
  • AI is expensive: Each call costs money. Event-driven architecture enables batching, prioritization, and rate management.
  • AI triggers cascade: One AI result often triggers follow-up processing (guardrails check, logging, notification, caching). Events enable clean separation.
💡
Event-driven does not replace request-response: For simple, fast AI calls (sub-second classification, short completions), request-response is fine. Event-driven architecture is best when processing is long-running, asynchronous, or needs to trigger downstream actions.

Event Sources for AI Systems

AI processing can be triggered by many types of events beyond user requests:

Event Source Example AI Processing Triggered
Webhooks GitHub push, Stripe payment, Slack message Code review, fraud detection, chatbot response
Message queues SQS message, Kafka event, RabbitMQ message Document processing, data enrichment, ETL
File uploads S3 object created, GCS file upload Image analysis, document OCR, video transcription
Database changes New row inserted, record updated, CDC event Content moderation, classification, summarization
Scheduled (cron) Daily at 2am, every 15 minutes Batch analysis, report generation, model retraining
User actions Form submission, button click, page view Personalization, recommendation, real-time assistance
IoT sensors Temperature reading, camera frame, vibration data Anomaly detection, predictive maintenance, safety alerts

Async Processing with Queues

The core event-driven AI architecture uses a message queue to decouple the request from the processing:

  1. Producer: Receives the trigger event and publishes a message to the queue. Returns immediately with a job ID.
  2. Queue: Stores messages durably, handles ordering, and manages delivery guarantees (at-least-once, exactly-once).
  3. Consumer/Worker: Picks messages from the queue, calls the AI model, processes the result, and publishes completion events.
  4. Notification: When processing completes, notify the original requester via webhook, WebSocket, polling endpoint, or push notification.

Event-Driven Document Processor

This implementation shows a complete event-driven pipeline that processes uploaded documents through AI analysis:

Python - Event-Driven Document Processor
import asyncio
import uuid
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from typing import Callable, Optional
from collections import defaultdict


class JobStatus(Enum):
    QUEUED = "queued"
    PROCESSING = "processing"
    COMPLETED = "completed"
    FAILED = "failed"


@dataclass
class Event:
    type: str
    payload: dict
    id: str = field(default_factory=lambda: str(uuid.uuid4()))
    timestamp: datetime = field(default_factory=datetime.now)


@dataclass
class Job:
    id: str = field(default_factory=lambda: str(uuid.uuid4()))
    event: Event = None
    status: JobStatus = JobStatus.QUEUED
    result: Optional[dict] = None
    error: Optional[str] = None
    created_at: datetime = field(default_factory=datetime.now)
    completed_at: Optional[datetime] = None


class EventBus:
    """Simple in-process event bus for event-driven AI."""

    def __init__(self):
        self.handlers: dict[str, list[Callable]] = defaultdict(list)
        self.event_log: list[Event] = []

    def subscribe(self, event_type: str, handler: Callable):
        """Register a handler for an event type."""
        self.handlers[event_type].append(handler)

    async def publish(self, event: Event):
        """Publish an event to all subscribers."""
        self.event_log.append(event)
        handlers = self.handlers.get(event.type, [])
        tasks = [handler(event) for handler in handlers]
        if tasks:
            await asyncio.gather(*tasks,
                                 return_exceptions=True)


class AsyncJobQueue:
    """Async job queue for AI processing tasks."""

    def __init__(self, max_workers: int = 3):
        self.queue: asyncio.Queue = asyncio.Queue()
        self.jobs: dict[str, Job] = {}
        self.max_workers = max_workers
        self.event_bus = EventBus()
        self._workers_started = False

    async def submit(self, event: Event) -> str:
        """Submit an event for async processing."""
        job = Job(event=event)
        self.jobs[job.id] = job
        await self.queue.put(job)

        # Publish job queued event
        await self.event_bus.publish(Event(
            type="job.queued",
            payload={"job_id": job.id,
                     "event_type": event.type}
        ))
        return job.id

    def get_status(self, job_id: str) -> Optional[dict]:
        """Check the status of a submitted job."""
        job = self.jobs.get(job_id)
        if not job:
            return None
        return {
            "job_id": job.id,
            "status": job.status.value,
            "result": job.result,
            "error": job.error,
            "created_at": job.created_at.isoformat(),
            "completed_at": (job.completed_at.isoformat()
                            if job.completed_at else None)
        }

    async def _worker(self, worker_id: int,
                      process_fn: Callable):
        """Worker that processes jobs from the queue."""
        while True:
            job = await self.queue.get()
            job.status = JobStatus.PROCESSING

            await self.event_bus.publish(Event(
                type="job.processing",
                payload={"job_id": job.id,
                         "worker": worker_id}
            ))

            try:
                result = await process_fn(job.event)
                job.status = JobStatus.COMPLETED
                job.result = result
                job.completed_at = datetime.now()

                await self.event_bus.publish(Event(
                    type="job.completed",
                    payload={"job_id": job.id,
                             "result": result}
                ))
            except Exception as e:
                job.status = JobStatus.FAILED
                job.error = str(e)
                job.completed_at = datetime.now()

                await self.event_bus.publish(Event(
                    type="job.failed",
                    payload={"job_id": job.id,
                             "error": str(e)}
                ))
            finally:
                self.queue.task_done()

    async def start_workers(self, process_fn: Callable):
        """Start worker tasks to process the queue."""
        if self._workers_started:
            return
        self._workers_started = True
        for i in range(self.max_workers):
            asyncio.create_task(
                self._worker(i, process_fn)
            )

Streaming LLM Responses with Server-Sent Events

Streaming lets users see AI output as it is generated, token by token, rather than waiting for the complete response. This creates a much more responsive user experience:

Python - Streaming with FastAPI + SSE
from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse
import anthropic
import json

app = FastAPI()


async def stream_claude_response(prompt: str):
    """Stream Claude's response token by token via SSE."""
    client = anthropic.AsyncAnthropic()

    # SSE format: each event is "data: ...\n\n"
    async with client.messages.stream(
        model="claude-sonnet-4-20250514",
        max_tokens=1024,
        messages=[{"role": "user", "content": prompt}]
    ) as stream:
        async for text in stream.text_stream:
            # Format as Server-Sent Event
            event_data = json.dumps({"text": text})
            yield f"data: {event_data}\n\n"

    # Send completion event
    yield f"data: {json.dumps({'done': True})}\n\n"


@app.post("/api/chat/stream")
async def chat_stream(request: Request):
    """Endpoint that streams AI responses via SSE."""
    body = await request.json()
    prompt = body.get("prompt", "")

    return StreamingResponse(
        stream_claude_response(prompt),
        media_type="text/event-stream",
        headers={
            "Cache-Control": "no-cache",
            "Connection": "keep-alive",
            "X-Accel-Buffering": "no"  # Disable nginx buffering
        }
    )


# Client-side JavaScript to consume the stream
CLIENT_JS = """
async function streamChat(prompt) {
    const response = await fetch('/api/chat/stream', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ prompt })
    });

    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    let buffer = '';

    while (true) {
        const { done, value } = await reader.read();
        if (done) break;

        buffer += decoder.decode(value, { stream: true });
        const lines = buffer.split('\\n\\n');
        buffer = lines.pop();  // Keep incomplete line

        for (const line of lines) {
            if (line.startsWith('data: ')) {
                const data = JSON.parse(line.slice(6));
                if (data.done) {
                    console.log('Stream complete');
                } else {
                    // Append token to the UI
                    document.getElementById('output')
                        .textContent += data.text;
                }
            }
        }
    }
}
"""

Webhook-Triggered AI Pipelines

Webhooks enable external services to trigger AI processing automatically. Common patterns include:

  • GitHub webhooks: Trigger AI code review on every pull request, auto-generate release notes on tag push, analyze commit messages for compliance
  • Slack webhooks: Process messages for sentiment analysis, auto-categorize support requests, translate messages in real-time
  • Stripe webhooks: Analyze purchase patterns for fraud detection, generate personalized follow-up emails, predict churn risk
  • CRM webhooks: Score new leads with AI, generate meeting summaries from transcripts, predict deal outcomes
Webhook best practices for AI: Always respond to webhooks within 3-5 seconds with a 200 status. Enqueue the actual AI processing for async handling. Implement idempotency (handle duplicate deliveries). Verify webhook signatures to prevent spoofing.

The Streaming Pattern

Streaming is a specific form of event-driven AI where the model's output is delivered incrementally as it is generated. There are two main streaming technologies:

Server-Sent Events (SSE)

  • One-way: server pushes events to client over a single HTTP connection
  • Simple to implement with standard HTTP
  • Auto-reconnection built into the browser's EventSource API
  • Best for: LLM response streaming, progress updates, notification feeds

WebSockets

  • Two-way: both client and server can send messages at any time
  • Lower overhead per message after initial handshake
  • Required when the client needs to send messages during the stream (e.g., cancellation, follow-up questions)
  • Best for: Interactive chat, collaborative editing, real-time dashboards

CQRS Pattern for AI

Command Query Responsibility Segregation (CQRS) separates read and write operations, and it adapts well to AI systems where writes (AI processing) are expensive and slow, but reads (retrieving results) should be fast:

  • Command side (Write): Accept AI processing requests, enqueue them, and process asynchronously. The write path handles the expensive AI calls.
  • Query side (Read): Serve pre-computed AI results from a read-optimized store (cache, database, search index). The read path is fast and cheap.
  • Event sync: When the command side completes processing, it publishes an event that updates the query side's data store.
💡
CQRS + AI example: A product catalog where AI generates descriptions. The command side processes new products through an LLM to generate descriptions (slow, expensive). The query side serves pre-generated descriptions from a database (fast, cheap). Users see instant results because the AI work was done ahead of time.

Comparison: Request-Response vs. Event-Driven vs. Streaming

Aspect Request-Response Event-Driven Streaming
Latency to first byte High (wait for full response) Low (immediate acknowledgment) Low (tokens arrive immediately)
Connection model Synchronous, blocking Async, fire-and-forget Long-lived, incremental
Error handling Simple (HTTP status codes) Complex (dead letter queues, retries) Moderate (reconnection, partial results)
Scalability Limited by timeout and threads High (workers scale independently) Moderate (long-lived connections)
User experience Wait, then see result Immediate ack, poll or notify See result appearing in real-time
Best for Fast, simple AI calls Background processing, pipelines Chat interfaces, real-time output
Combine all three: Most production AI systems use all three patterns. The chat interface uses streaming for real-time responses. Background tasks like document processing use event-driven queues. Simple API endpoints for classification or embedding use request-response. Choose the pattern that matches each use case's requirements.

Ready to Go Deeper?

Live instructor-led courses from our partners. Affiliate disclosure.