How to Use PostgreSQL for LangGraph Memory and Checkpointing with FastAPI

Langchainlanggraphcheckpointerai agentsAI
How to Use PostgreSQL for LangGraph Memory and Checkpointing with FastAPI

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.