OpenAI Agents SDK Sessions: Persistent State in Production
OpenAI Agents forget everything between runs by default. Here's how to wire SQLiteSession for dev, Redis for production, and wrap it in a FastAPI endpoint — with working code.
Runner.run() is stateless. Every call starts from a blank slate. If you’re prototyping, this is fine — but the moment you build anything with a conversation loop, you hit a wall. The agent introduces itself on every message. It asks “how can I help?” to someone who’s been chatting for ten minutes.
The fix is Sessions — a built-in persistence layer in the OpenAI Agents SDK that stores conversation history, supports multiple backends, and resumes interrupted runs. Our deep dive into the SDK’s architecture covers the why. This tutorial covers the how, with runnable code you can take straight to production.
Every line below works with openai-agents>=0.0.7 and Python 3.10+.
The problem: stateless by default
import asyncio
from agents import Agent, Runner
agent = Agent(name="Assistant", instructions="You are a helpful assistant.")
# First message — normal
result = asyncio.run(Runner.run(agent, "My name is Alice."))
print(result.final_output)
# "Hello Alice! How can I help you?"
# Second message — amnesia
result = asyncio.run(Runner.run(agent, "What's my name?"))
print(result.final_output)
# "I don't know your name. What is it?"
Every Runner.run() call is an island. The agent receives a single input string, processes it, and returns. No conversation history survives between calls. This is by design — the SDK keeps the core execution loop simple — but it’s the first thing that trips up developers moving past the quickstart.
For a deeper look at how the runner actually executes agents under the hood (the tool-call loop, state management, and streaming), see the official Running Agents docs.
Step 1: Add a SQLiteSession (dev and single-server)
The simplest path to persistence: pass a SQLiteSession to the runner. It stores conversation history in a local SQLite file and reconstructs context on each run.
pip install openai-agents
import asyncio
from agents import Agent, Runner
from agents.sessions import SQLiteSession
session = SQLiteSession("chat.db")
agent = Agent(
name="Persistent Assistant",
instructions="You are a helpful assistant. Be concise.",
)
async def main():
# First message
result = await Runner.run(
agent,
"My name is Alice and I live in Berlin.",
session=session,
)
print(result.final_output)
# "Hi Alice from Berlin! How can I help?"
# Second message — session holds the history
result2 = await Runner.run(
agent,
"What's my name and where do I live?",
session=session,
)
print(result2.final_output)
# "Your name is Alice and you live in Berlin."
asyncio.run(main())
That’s it. The session automatically persists every RunResult to SQLite and feeds prior conversation items back into the context on subsequent calls. No manual history management.
How sessions actually merge history
By default, sessions use "last_n" mode: retrieve the most recent items from the stored history and prepend them to the new input. You can control this via SessionConfig:
from agents.sessions import SessionConfig, SQLiteSession
session = SQLiteSession(
"chat.db",
config=SessionConfig(
# Keep last 20 items only — prevents context bloat
item_limit=20,
# Or use "include_all" to send full history every time
# history_mode="include_all",
),
)
The item_limit is critical in production. Without it, a long-running conversation accumulates hundreds of tool calls and responses, blowing past your model’s context window. We’ve seen teams hit this silently — the model degrades, outputs get truncated, and nobody notices until a user complains.
Step 2: Wire RedisSession for multi-server deployments
SQLite works on a single machine. If you’re running behind a load balancer, you need a shared session store. The SDK ships a RedisSession in the openai-agents extensions:
pip install openai-agents redis
import os
import asyncio
from redis.asyncio import Redis
from agents import Agent, Runner
from agents.extensions.memory import RedisSession
redis = Redis.from_url(os.environ["REDIS_URL"])
session = RedisSession(redis_client=redis, key_prefix="agent:sessions:")
agent = Agent(
name="Production Agent",
instructions="You are a helpful assistant.",
)
async def main():
# Session ID ties conversations to users
user_id = "user-abc-123"
result = await Runner.run(
agent,
"I prefer dark mode and metric units.",
session=session,
session_id=user_id,
)
# Any server in the cluster can now pick up this session
result2 = await Runner.run(
agent,
"What are my preferences?",
session=session,
session_id=user_id,
)
print(result2.final_output)
# "You prefer dark mode and metric units."
await redis.aclose()
asyncio.run(main())
The session_id parameter is the key insight here. In a SQLite setup, you can get away with omitting it (the session uses a default). In Redis, you must pass a per-user session_id so the session can isolate conversations. Without it, every request to RedisSession shares the same key and you get a chaotic blend of user conversations.
The SDK also ships MongoDBSession, DaprSession, SQLAlchemySession, and EncryptedSession as built-in backends — all following the same pattern. Pick the one that matches your stack. Full list in the official sessions documentation.
Step 3: Wrap it in FastAPI
Here’s a complete production endpoint that ties it together:
import os
import asyncio
from typing import Optional
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from redis.asyncio import Redis
from agents import Agent, Runner
from agents.extensions.memory import RedisSession
app = FastAPI()
# --- Agent definition ---
agent = Agent(
name="Support Agent",
instructions=(
"You are a customer support agent for a SaaS platform. "
"Be concise, helpful, and remember context from earlier in the conversation."
),
)
# --- Session store (connect once at startup) ---
session: Optional[RedisSession] = None
@app.on_event("startup")
async def startup():
global session
redis = Redis.from_url(
os.environ["REDIS_URL"],
decode_responses=False,
)
session = RedisSession(redis_client=redis, key_prefix="support:sessions:")
@app.on_event("shutdown")
async def shutdown():
if session is not None:
await session.redis_client.aclose()
# --- Request/response models ---
class ChatRequest(BaseModel):
user_id: str
message: str
class ChatResponse(BaseModel):
reply: str
# --- Endpoint ---
@app.post("/chat", response_model=ChatResponse)
async def chat(req: ChatRequest):
if session is None:
raise HTTPException(status_code=503, detail="Session store not ready")
result = await Runner.run(
agent,
req.message,
session=session,
session_id=req.user_id,
)
return ChatResponse(reply=result.final_output)
Run it:
export OPENAI_API_KEY=sk-...
export REDIS_URL=redis://localhost:6379
uvicorn app:app --reload
Test it with two sequential calls:
curl -X POST http://localhost:8000/chat \
-H "Content-Type: application/json" \
-d '{"user_id": "alice", "message": "My account email is alice@example.com."}'
curl -X POST http://localhost:8000/chat \
-H "Content-Type: application/json" \
-d '{"user_id": "alice", "message": "What email is on my account?"}'
The second response should reference alice@example.com. The session carries the conversation forward, even across HTTP requests.
What sessions don’t do
Sessions persist conversation items — messages, tool calls, results. They do not extract structured facts, build knowledge graphs, or retrieve context semantically. If your agent needs to remember facts about a user across entirely separate conversation threads (not just the current thread), you need a memory layer on top — something like Mem0, Zep, or LangMem.
The distinction matters:
| Concern | Solved by |
|---|---|
| ”What did the user just say in this conversation?” | Sessions |
| ”What are all the preferences this user has ever expressed?” | Memory layer |
| ”What happened in that agent run?” (debugging) | Tracing |
For tracing, the SDK integrates with OpenAI’s built-in tracing dashboard and supports custom processors for exporting to LangFuse, Arize, and others — we covered that in our LangFuse + OpenAI Agents tutorial.
When to use which backend
A quick decision guide:
| Backend | When to use |
|---|---|
SQLiteSession | Local dev, single-server deployments, prototyping |
RedisSession | Multi-server, low-latency, TTL-based expiry |
MongoDBSession | Document-oriented, audit trails, long-lived sessions |
SQLAlchemySession | Teams already on Postgres/MySQL, ACID requirements |
EncryptedSession | Compliance (wrapping any backend with encryption at rest) |
AdvancedSQLiteSession | Single-server with WAL mode, better concurrency than vanilla SQLite |
Our team defaults to Redis for most production workloads: it’s fast, TTLs handle cleanup automatically, and every cloud provider offers a managed Redis. For regulated environments where conversation history must survive audits, we layer EncryptedSession on top of MongoDBSession or SQLAlchemySession.
One session, one conversation thread
A common mistake: using the same session_id for multiple conversation threads belonging to the same user. If the user opens two browser tabs and both share session_id="alice", their conversations interleave unpredictably.
The fix: generate a unique session ID per conversation thread, not per user. Use the user ID as a prefix for traceability:
import uuid
session_id = f"user:{user_id}:thread:{uuid.uuid4().hex[:12]}"
This keeps conversations isolated while still letting you query all sessions for a given user from Redis (using SCAN on the key prefix).
Next steps
Sessions solve the persistence problem. The next layer is memory — extracting structured knowledge that survives across sessions. We covered that in our agent memory comparison and in the LangGraph state + checkpointing tutorial for those using LangGraph instead of the OpenAI SDK.
If you’re building a multi-agent system with handoffs, our OpenAI Agents SDK tools, guardrails, and handoffs tutorial walks through the full customer-support triage pattern with the same SDK.
Related Posts
Build a Finance AI Agent with OpenAI Agents SDK: Portfolio Analysis & Risk Assessment
Build a multi-agent portfolio analyst with the OpenAI Agents SDK — market data lookup, risk scoring, portfolio rebalancing tools, and a specialist handoff architecture.
OpenAI Agents SDK Tutorial: Tools, Guardrails & Handoffs
Build a multi-agent support system with the OpenAI Agents SDK: custom tools, guardrails, handoffs, and human-in-the-loop approval in one Python file.
OpenAI Agents SDK: Deep Dive for Production Agent Builders
Hands-on deep dive into OpenAI Agents SDK architecture: agents, handoffs, guardrails, sandbox execution, and production patterns.