When you're building an agentic system, one of the earliest architectural decisions, and one that's surprisingly easy to get wrong, is how the agent's main loop runs. Does it sit idle, wake up when a request arrives, do the work, and return a response? Or does it maintain a continuous loop, subscribing to event streams and reacting as things happen? This isn't a cosmetic choice. It shapes concurrency, state management, latency profile, and how you handle failure throughout the entire system.
Most teams start with request-driven agents because the model maps directly onto familiar REST API patterns. A request comes in, the agent processes it, a response goes out. Comfortable, well-understood, easy to trace. Event-driven agents are less familiar and more complex, but they unlock behaviors that request-driven simply can't provide, and some use cases genuinely require them.
Two Models of Agent Time
The fundamental difference between the two models is about existence: a request-driven agent exists only during a request. It wakes up when a request arrives, does its work, returns a response, and effectively ceases to exist until the next request comes in. It has no continuous relationship with the world between requests.
An event-driven agent has continuous existence. It maintains a live loop, subscribed to event streams, and reacts to things as they happen. When nothing is happening, it waits. When an event arrives, from any source, it processes it. That persistent context, accumulating across events, changes what the agent can do in ways that feel small until you actually need them.
The Request-Driven Model
In the request-driven model, each interaction is a self-contained unit. Request arrives, agent loads context from storage, runs its LLM call and tool loop, persists the results, returns the response. Clean start, clean end.
# Request-driven agent, clean, predictable, easy to trace
from fastapi import FastAPI
from pydantic import BaseModel
import anthropic
app = FastAPI()
client = anthropic.Anthropic()
class AgentRequest(BaseModel):
session_id: str
message: str
@app.post("/agent")
async def handle_request(req: AgentRequest):
# Load conversation history from store
history = await state_store.get(req.session_id) or []
history.append({"role": "user", "content": req.message})
# One LLM call + tool execution loop
response = await run_agent_loop(history)
# Persist updated history
history.append({"role": "assistant", "content": response})
await state_store.set(req.session_id, history)
return {"response": response}
# Each POST is a discrete, traceable unit of work
# The agent does not exist between requestsThe operational profile here is genuinely nice. Every request is a discrete unit of work with a clear start, a clear end, and an independently traceable execution path. When something goes wrong, you look at one request. You know exactly what state the system was in. Retrying failed requests is safe and simple. This is why most teams default here, and for many use cases it's absolutely the right call.
The Event-Driven Model
In the event-driven model, the agent subscribes to event streams and keeps a live loop running. Events can come from anywhere: user messages, tool completion notifications, scheduled cron triggers, webhooks from external services, messages from other agents in the system. The agent reacts to each event as it arrives.
# Event-driven agent, persistent loop, reactive to any event type
import asyncio
from typing import AsyncIterator
class EventDrivenAgent:
def __init__(self, event_bus, state_store):
self.bus = event_bus
self.store = state_store
self.context = {} # accumulated context across events
async def run(self):
"""Continuous event processing loop."""
async for event in self.bus.subscribe(["user.*", "tool.*", "cron.*"]):
try:
await self.handle_event(event)
except Exception as e:
await self.bus.publish("agent.error", {"event": event, "error": str(e)})
async def handle_event(self, event: dict):
event_type = event["type"]
if event_type == "user.message":
# React to user input, potentially emitting multiple downstream events
response = await self.process_user_message(event["payload"])
await self.bus.publish("agent.response", response)
elif event_type == "tool.result":
# React to a tool completing, maybe trigger next step
await self.process_tool_result(event["payload"])
elif event_type == "cron.tick":
# Proactive behavior without any user trigger
await self.run_scheduled_check()
# Context accumulates across all event types
self.context["last_event"] = eventNow here's where it gets interesting, event-driven agents can do something request-driven agents fundamentally cannot: proactive behavior. An event-driven agent can wake up on a scheduled tick, check a condition, and take action without any user touching it. It can react immediately when a long-running tool finishes, rather than polling. These behaviors require the continuous loop. You can't retrofit them onto a request-driven architecture without significant contortion.
Performance Characteristics
For latency-sensitive workloads, event-driven has a structural edge. A request-driven agent loads context from scratch on every request, that's a round-trip to the state store before the first LLM call even starts. An event-driven agent with pre-loaded context sitting in memory can react to an event with a much shorter processing path for simple reactions.
Throughput is more nuanced. Request-driven agents scale horizontally with almost zero friction, add instances behind a load balancer and throughput scales linearly. Event-driven agents are trickier. If your agent keeps per-session state in memory, you can't just add instances without solving how that state is partitioned. Kafka consumer groups and Redis-backed context are common solutions, but they add operational complexity that request-driven avoids entirely.
State Management Implications
State management is where the two models diverge most sharply. Request-driven treats state as external: load it at the top of the request, persist it at the bottom. The agent is stateless between requests. This is operationally clean, but it forces a state-store round-trip on every single interaction.
Event-driven agents can keep hot state in memory across events, much faster, but fragile. If the agent process crashes, in-memory state is gone. If you're running multiple instances, each has its own in-memory state and you need session routing to ensure events for the same session reach the same instance. These are solvable, but they're real problems you have to plan for upfront.
When Each Model Makes Sense
Go with request-driven when your agent handles discrete, user-initiated tasks with clear start and end points. Customer support bots, document processors, code reviewers, data analyzers, these all fit the model well. The tasks are bounded, interactions are user-initiated, and the clean operational profile makes the system dramatically easier to debug and scale.
Switch to event-driven when your agent needs to react to events from multiple sources, maintain awareness between interactions, or do things proactively without a user triggering it. Monitoring agents watching for anomalies, pipeline agents reacting to upstream data, autonomous assistants that check conditions on a schedule, these require the continuous loop. You can't fake it with request-driven.
Hybrid Architectures
In practice, most production systems end up using both models for different parts of the same workflow. The user-facing chatbot runs request-driven, it fits the interaction model perfectly and the operational simplicity is worth it. Behind it, a monitoring agent runs an event-driven loop, watching conversation patterns and triggering escalations or data updates when conditions are met.
The key is matching the model to the interaction pattern, not picking one architecture for everything. Request-driven for user-initiated discrete tasks. Event-driven for continuous reactive behavior and cross-agent coordination where you need things to happen without a human pushing a button.
Implementation Patterns
When building event-driven agents, the three decisions that matter most are: the event schema (keep it small, version it from day one), backpressure handling (what happens when events arrive faster than the agent can process them), and ordering guarantees within a session while allowing parallel processing across sessions.
# Practical event schema for agent systems
from pydantic import BaseModel
from datetime import datetime
from typing import Any
import uuid
class AgentEvent(BaseModel):
event_id: str = str(uuid.uuid4())
type: str # dot-notation: "user.message", "tool.result"
session_id: str # for routing to the right agent instance
trace_id: str # for distributed tracing across agents
timestamp: datetime
payload: dict[str, Any]
version: str = "1.0" # schema version for backward compatibility
# Event bus with backpressure
class BoundedEventBus:
def __init__(self, max_queue_size: int = 1000):
self.queues: dict[str, asyncio.Queue] = {}
self.max_size = max_queue_size
async def publish(self, event: AgentEvent):
queue = self.queues.setdefault(event.session_id, asyncio.Queue(self.max_size))
try:
queue.put_nowait(event)
except asyncio.QueueFull:
# Backpressure: drop or route to overflow handler
await self.overflow_handler(event)
async def subscribe(self, session_id: str) -> AsyncIterator[AgentEvent]:
queue = self.queues.setdefault(session_id, asyncio.Queue(self.max_size))
while True:
event = await queue.get()
yield eventThe Practical Decision
Request-driven is the right default. It's simpler, more predictable, and scales horizontally without friction. Start there unless your requirements clearly call for event-driven behavior, proactive actions, multi-source event handling, continuous context accumulation.
When you do need event-driven, invest in the event schema and state management foundations early. Retrofitting a solid schema and backpressure mechanism onto an event-driven system that started without them is painful work that always happens at the worst possible time.
Frequently Asked Questions
Build secure agentic systems with AgentixForce
We help companies design, build, and secure production-grade AI agent systems. From architecture reviews to full implementation, our team brings deep expertise to every engagement.