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 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:
- Producer: Receives the trigger event and publishes a message to the queue. Returns immediately with a job ID.
- Queue: Stores messages durably, handles ordering, and manages delivery guarantees (at-least-once, exactly-once).
- Consumer/Worker: Picks messages from the queue, calls the AI model, processes the result, and publishes completion events.
- 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:
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:
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
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.
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 |
Ready to Go Deeper?
Live instructor-led courses from our partners. Affiliate disclosure.
AI & ML Courses - 30% Off
Live instructor-led AI, machine learning, data science, and cloud courses for working professionals. Use code Limited30 at checkout.
EdurekaDataCamp - AI & Data Science
Hands-on Python, machine learning, and AI courses with interactive exercises and real projects.
DataCampedX - Top AI Courses
University-level AI courses from MIT, Harvard, Stanford. Earn certificates that employers recognize.
edX