LangGraph AsyncPostgresSaver: fixing 'psycopg.OperationalError: the connection is closed' in FastAPI
The checkpointer's connection closes when the async with block exits, but the compiled graph keeps using it. Keep the connection open for the app's lifetime.
Updated 2026-10-29.
Symptom: your LangGraph app works with MemorySaver, but after you switch to AsyncPostgresSaver behind FastAPI, the first request that touches a checkpoint raises psycopg.OperationalError: the connection is closed. The cause is where the async with block ends, not your database. The fix is to open the saver (or a pool) in the FastAPI lifespan.
Disclosure: I work on MemorySync (I'm Mohammed, the founder). I drafted this page with AI assistance and checked it against the source issue and the official docs listed under Sources on Oct 28, 2026. I read the code and docs; I didn't run this FastAPI app for this page.
The question
In langchain-ai/langgraph#1292, "Encountering OperationalError with Postgres Checkpointer in LangGraph" (Aug 9, 2024), the Postgres checkpointer failed with:
psycopg.OperationalError: the connection is closed
The graph was compiled inside a function like this:
async with AsyncPostgresSaver.from_conn_string(os.environ.get("POSTGRES_CONN_STRING")) as checkpointer:
return workflow.compile(
checkpointer=checkpointer,
interrupt_before=[AgentRole.USER_INPUT],
)
A later commenter asked the follow-up most FastAPI users have: how to compile the graph once in the lifespan and reuse it for every request, instead of compiling per request.
Status (checked Oct 28, 2026 via the GitHub API): closed Aug 9, 2024, after the reply explaining the cause below. The FastAPI lifespan follow-up (Sep 2024, bumped Mar 2025) has no answer in the thread.
Why it happens
from_conn_string is an async context manager. The connection is open only inside the async with block. Returning the compiled graph from inside the block exits it, which closes the connection, but the graph still holds the checkpointer. The first request that reads or writes a checkpoint then hits psycopg's closed connection. As the reply on #1292 put it: when the function returns, "it is already out of the with scope, at which point the database connection has already been closed."
Fix 1: keep the block open for the app's lifetime
Enter the context manager in the lifespan and yield inside it, so it closes only on shutdown:
from contextlib import asynccontextmanager
from fastapi import FastAPI
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
@asynccontextmanager
async def lifespan(app: FastAPI):
async with AsyncPostgresSaver.from_conn_string(DB_URI) as checkpointer:
await checkpointer.setup() # first run only: creates the tables
app.state.graph = builder.compile(checkpointer=checkpointer)
yield
app = FastAPI(lifespan=lifespan)
Fix 2: use a connection pool
A single connection serves one request at a time. AsyncPostgresSaver also accepts a psycopg AsyncConnectionPool (its connection type in the source is AsyncConnection | AsyncConnectionPool). The package README says manually created connections need autocommit=True and row_factory=dict_row:
from psycopg.rows import dict_row
from psycopg_pool import AsyncConnectionPool
@asynccontextmanager
async def lifespan(app: FastAPI):
async with AsyncConnectionPool(
DB_URI,
kwargs={"autocommit": True, "row_factory": dict_row},
) as pool:
checkpointer = AsyncPostgresSaver(pool)
await checkpointer.setup()
app.state.graph = builder.compile(checkpointer=checkpointer)
yield
The source raises an error if you combine a pool with pipeline; pipelines are for a single connection only.
Example: lifespan pool plus a request handler
This puts Fix 2 together with a handler. The saver and pool calls follow the langgraph-checkpoint-postgres README (autocommit=True, row_factory=dict_row) and AsyncPostgresSaver in langgraph/checkpoint/postgres/aio.py. builder is your StateGraph. I didn't run this app.
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
from psycopg.rows import dict_row
from psycopg_pool import AsyncConnectionPool
DB_URI = "postgresql://user:pass@localhost:5432/app"
@asynccontextmanager
async def lifespan(app: FastAPI):
async with AsyncConnectionPool(
DB_URI,
open=False,
kwargs={"autocommit": True, "row_factory": dict_row},
) as pool: # opened here, closed only on shutdown
checkpointer = AsyncPostgresSaver(pool)
await checkpointer.setup()
app.state.graph = builder.compile(checkpointer=checkpointer)
yield
app = FastAPI(lifespan=lifespan)
@app.post("/chat/{thread_id}")
async def chat(thread_id: str, body: dict, request: Request):
graph = request.app.state.graph
config = {"configurable": {"thread_id": thread_id}}
return await graph.ainvoke({"messages": [("user", body["message"])]}, config)
open=False with async with lets the context manager open the pool, which avoids psycopg_pool's warning about opening a pool in the constructor.
Checklist
- No
returnof a compiled graph from inside anasync with ... as checkpointerblock. - Compile once in the lifespan, read
app.state.graphin handlers. - Call
setup()the first time against a new database. - Manual connections or pools:
autocommit=Trueandrow_factory=dict_row.
Common questions
Why does it work in a script?
In a script you usually run the graph inside the async with block, so the connection is still open.
Does MemorySaver have this problem?
No: the in-memory saver has no connection to close, which is why the app works until you switch to Postgres.
Is the checkpointer my agent's long-term memory? It's per-thread state (resume, time travel). Memory across threads is a separate store.
If you also need memory across threads
The MemorySync LangGraph guide keeps the two apart: thread state stays on a checkpointer such as langgraph-checkpoint-postgres, and MemorySync supplies long-term memory across threads through a LangGraph BaseStore (MemorySyncStore) passed as store=, plus context injection, a persist node and a search tool. It doesn't replace the checkpointer, so the fix above still applies. Read the guide first. To see save and recall in your browser, try the MemorySync playground.
Related pages
- n8n WhatsApp AI agent memory: which Session ID to use
- n8n Postgres Chat Memory: fixing 'Got unexpected type: user'
- Codex CLI persistent memory with AGENTS.md and MCP
Sources
- langchain-ai/langgraph#1292 (closed 2024-08-09; read 2026-10-28)
- langgraph
libs/checkpoint-postgres/README.mdandlanggraph/checkpoint/postgres/aio.py,_ainternal.pyon main (read 2026-10-28; PyPI latest langgraph-checkpoint-postgres 3.2.0) - MemorySync docs: docs.memorysync.io/guides/langgraph
About the author
- Building MemorySync (memorysync.io) — persistent memory infrastructure for AI agents, Cursor, and Claude Code.
Comments
Join the discussionNo comments yet. Be the first to comment.
