# LangGraph AsyncPostgresSaver: fixing 'psycopg.OperationalError: the connection is closed' in FastAPI

*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](https://github.com/langchain-ai/langgraph/issues/1292), "Encountering OperationalError with Postgres Checkpointer in LangGraph" (Aug 9, 2024), the Postgres checkpointer failed with:

```text
psycopg.OperationalError: the connection is closed
```

The graph was compiled inside a function like this:

```python
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:

```python
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`:

```python
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.*

```python
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

1. No `return` of a compiled graph from inside an `async with ... as checkpointer` block.
2. Compile once in the lifespan, read `app.state.graph` in handlers.
3. Call `setup()` the first time against a new database.
4. Manual connections or pools: `autocommit=True` and `row_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](https://docs.memorysync.io/guides/langgraph?utm_source=hashnode&utm_medium=organic_page&utm_campaign=v5_oct25&utm_content=pseo_langgraph-asyncpostgressaver-connection-is-closed-fastapi) 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](https://docs.memorysync.io/guides/langgraph?utm_source=hashnode&utm_medium=organic_page&utm_campaign=v5_oct25&utm_content=pseo_langgraph-asyncpostgressaver-connection-is-closed-fastapi) first. To see save and recall in your browser, try the [MemorySync playground](https://memorysync.io/try?utm_source=hashnode&utm_medium=organic_page&utm_campaign=v5_oct25&utm_content=pseo_langgraph-asyncpostgressaver-connection-is-closed-fastapi).

## Related pages

- [n8n WhatsApp AI agent memory: which Session ID to use](https://memorysync.hashnode.dev/n8n-whatsapp-ai-agent-memory-session-id)
- [n8n Postgres Chat Memory: fixing 'Got unexpected type: user'](https://memorysync.hashnode.dev/n8n-postgres-chat-memory-unexpected-type-user)
- [Codex CLI persistent memory with AGENTS.md and MCP](https://memorysync.hashnode.dev/codex-cli-persistent-memory-agents-md-mcp)

## Sources

- langchain-ai/langgraph#1292 (closed 2024-08-09; read 2026-10-28)
- langgraph `libs/checkpoint-postgres/README.md` and `langgraph/checkpoint/postgres/aio.py`, `_ainternal.py` on main (read 2026-10-28; PyPI latest langgraph-checkpoint-postgres 3.2.0)
- MemorySync docs: docs.memorysync.io/guides/langgraph
