
I Built a LangGraph + FastAPI Agent… and Spent Days Fighting Postgres
Here’s what actually went wrong — and how I fixed it.
I had a LangGraph-based AI agent working perfectly.
- ✅ Memory worked
- ✅ Checkpointing worked
- ✅ Postgres worked
Everything ran flawlessly in a standalone Python script.
Then I moved the exact same logic into FastAPI.
And everything broke.
This post explains the errors I encountered, why they happened, and the one mental model shift that ultimately solved everything.
The Goal
The architecture seemed straightforward:
- Use LangGraph for stateful conversations
- Store memory and checkpoints in Postgres
- Run everything inside FastAPI for production
In a standalone script, the implementation looked something like this:
async with (
AsyncPostgresStore.from_conn_string(DB_URI) as store,
AsyncPostgresSaver.from_conn_string(DB_URI) as checkpointer,
):
graph = builder.compile(
store=store,
checkpointer=checkpointer
)
await graph.ainvoke(...)
Simple.
Clean.
And it worked perfectly.
So naturally, I reused the same approach inside FastAPI.
That's when the problems began.
Error #1: "the connection is closed"
The first error looked like a typical database issue:
psycopg.OperationalError: the connection is closed
My first assumptions were:
- Connection pool exhaustion
- Event loop conflicts
- PostgreSQL configuration limits
None of those were the real cause.
The Real Issue
Inside FastAPI, I compiled the graph during application startup using an async with block.
Something like:
async with (
AsyncPostgresStore.from_conn_string(DB_URI) as store,
AsyncPostgresSaver.from_conn_string(DB_URI) as checkpointer,
):
app.state.graph = builder.compile(...)
What actually happened was:
Open connections
↓
Compile graph
↓
Exit async with
↓
Connections close ❌
↓
Requests arrive later
↓
Graph tries to use closed connections
The graph itself remained alive.
The database resources did not.
Error #2: _AsyncGeneratorContextManager Has No Attribute get_next_version
After attempting a refactor, things became even more confusing.
I started seeing:
AttributeError:
'_AsyncGeneratorContextManager'
object has no attribute 'get_next_version'
At first glance, this looked like a LangGraph bug.
It wasn't.
Debugging the Types
I added some simple debugging:
print(type(store))
print(type(checkpointer))
The output was surprising:
<class 'contextlib._AsyncGeneratorContextManager'>
<class 'contextlib._AsyncGeneratorContextManager'>
The Key Realization
These methods:
AsyncPostgresStore.from_conn_string(...)
AsyncPostgresSaver.from_conn_string(...)
do not return usable store objects.
They return async context managers.
In a script, you immediately enter them:
async with AsyncPostgresStore.from_conn_string(...) as store:
so you rarely notice.
But in FastAPI, if you pass them directly into LangGraph without entering the context manager, LangGraph receives this:
_AsyncGeneratorContextManager
instead of:
AsyncPostgresStore
LangGraph expects methods such as:
checkpointer.get_next_version()
Context managers do not provide those methods.
Hence the error.
Error #3: relation "checkpoints" does not exist
Once the lifecycle issues were fixed, another error appeared:
psycopg.errors.UndefinedTable:
relation "checkpoints" does not exist
Fortunately, this one was easier to understand.
Why It Happened
I had commented out these lines:
await store.setup()
await checkpointer.setup()
Without them, PostgreSQL had no required tables.
LangGraph does not automatically create its schema.
The setup calls are mandatory.
The Mental Model That Fixed Everything
After chasing multiple errors, one realization made the entire problem obvious:
Scripts own their lifecycle. Servers do not.
Everything flowed from that idea.
How Scripts Work
A script typically follows this pattern:
Open resources
↓
Do work
↓
Close resources
↓
Exit
The lifecycle is short-lived and linear.
How FastAPI Works
A server follows a completely different lifecycle:
Start application
↓
Open shared resources
↓
Serve requests for hours or days
↓
Close resources on shutdown
Resources must remain alive for the entire application lifetime.
The Correct Production Pattern
The fix wasn't changing LangGraph.
The fix was moving resource ownership into FastAPI's lifespan.
1. Enter Postgres Context Managers Once
from contextlib import asynccontextmanager
@asynccontextmanager
async def lifespan(app: FastAPI):
async with (
AsyncPostgresStore.from_conn_string(DB_URI) as store,
AsyncPostgresSaver.from_conn_string(DB_URI) as checkpointer,
):
await store.setup()
await checkpointer.setup()
app.state.graph = await create_graph(
store,
checkpointer
)
yield
Resources remain alive until application shutdown.
2. Keep Graph Creation Pure
The graph builder should not manage database lifecycles.
async def create_graph(
store,
checkpointer,
):
builder = StateGraph(MessagesState)
builder.add_node(
"call_model",
call_model
)
builder.add_edge(
START,
"call_model"
)
return builder.compile(
store=store,
checkpointer=checkpointer,
)
Graph creation becomes deterministic and reusable.
3. Reuse the Graph Per Request
Each request simply retrieves the pre-built graph.
@app.post("/chat")
async def chat(request: Request):
graph = request.app.state.graph
return await graph.ainvoke(...)
No database initialization.
No graph recompilation.
No lifecycle confusion.
Why the Script Worked but FastAPI Didn't
The script effectively did this:
Open DB
↓
Run graph
↓
Close DB
↓
Exit
Everything happened within a single resource lifetime.
FastAPI requires:
Open DB
↓
Serve request
↓
Serve request
↓
Serve request
↓
Serve request
↓
Shutdown
↓
Close DB
The graph must hold references to resources that remain valid throughout the application's lifetime.
Once the lifecycle matched the framework, every error disappeared.
Lessons Learned
If you're integrating LangGraph with FastAPI and Postgres, these lessons can save you hours of debugging:
1. Don't Use async with for Long-Lived Shared Resources
If the resource must survive beyond the current scope, manage it at application startup.
2. from_conn_string() Returns Context Managers
This is easy to miss.
Always verify whether you're dealing with:
AsyncPostgresStore
or
_AsyncGeneratorContextManager
3. Run setup()
LangGraph does not automatically create Postgres tables.
Always initialize storage explicitly:
await store.setup()
await checkpointer.setup()
4. If You See _AsyncGeneratorContextManager, Stop
That's usually a strong signal that your lifecycle management is incorrect.
5. Scripts and Servers Require Different Thinking
Code that works perfectly in a script may fail inside a long-running service because the ownership model is fundamentally different.
Final Thoughts
This wasn't:
- A LangGraph bug
- A FastAPI bug
- A PostgreSQL bug
It was a lifecycle mismatch.
Once I stopped thinking like a script author and started thinking like a server author, the solution became straightforward.
If you're building stateful AI agents with LangGraph, Postgres, and FastAPI, spend time getting lifecycle management right first.
The graph, memory, and checkpointing pieces become remarkably simple once resource ownership is aligned with the framework.