Skip to main content

LangGraph checkpoints turn ZoneInfo datetimes into fixed offsets (DST math off by an hour after resume)

A zone-aware datetime in graph state comes back from the checkpointer with a fixed UTC offset. How to spot it and store times safely.

Updated 7 min readMarkdown

Disclosure: I work on MemorySync (I'm Mohammed, the founder). I drafted this page with AI assistance and checked it against the GitHub issue and the LangGraph source listed under Sources on Oct 30, 2026. Code status: I ran the repro, the graph check and the workaround offline on langgraph 1.2.18 with langgraph-checkpoint 4.3.1 and InMemorySaver (Python 3.13, no model, no network). I didn't run the Postgres, SQLite or Redis checkpointers, the fix branch linked in the issue, or any MemorySync code.

Your LangGraph state holds an aware datetime (an appointment, a deadline, a reminder) with a ZoneInfo timezone. Before a resume, adding a day lands on the right local time. After the graph is resumed from a checkpoint, the same arithmetic is an hour off across a daylight-saving switch, and nothing raises.

That's LangGraph issue #8826, "datetime silently loses its ZoneInfo (and fold) on checkpoint round-trip, so DST arithmetic after a resume is wrong".

MemorySync doesn't fix this bug. It's in how the checkpoint serializer encodes datetime, and you need the workaround below with or without MemorySync.

Skip the memory plumbing around this fix. Once your state stores times safely, the MemorySync LangGraph guide shows how to add memory per user across threads. It doesn't replace the checkpointer or change how it serializes state. When you're ready, see MemorySync. Founding offer: use code FOUNDING30 for 30% off Starter for your first 3 months (Settings → Billing).

Status (Nov 9, 2026)

  • #8826 was opened Sep 5, 2026 and is open. Several commenters reproduced it, and a fix branch is linked in the thread; it isn't merged.
  • It still reproduces on langgraph 1.2.18 with langgraph-checkpoint 4.3.1, which I ran (output below). As of Nov 9, 2026 the latest langgraph on PyPI is 1.2.20 (checkpoint still 4.3.1); I didn't re-run 1.2.20.

Step 1: Reproduce it

This is the issue's repro, run against JsonPlusSerializer, the serializer the checkpointers use:

import datetime as dt
from zoneinfo import ZoneInfo
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer

s = JsonPlusSerializer()
ny = ZoneInfo("America/New_York")

before = dt.datetime(2026, 3, 7, 9, 0, tzinfo=ny)   # day before the US DST switch
after = s.loads_typed(s.dumps_typed(before))

print(before == after)
print(repr(before.tzinfo), repr(after.tzinfo))
print((before + dt.timedelta(days=1)).astimezone(ny))
print((after + dt.timedelta(days=1)).astimezone(ny))

f = dt.datetime(2026, 11, 1, 1, 30, tzinfo=ny, fold=1)
print(f.fold, s.loads_typed(s.dumps_typed(f)).fold)

What I got on langgraph 1.2.18 / langgraph-checkpoint 4.3.1:

True
zoneinfo.ZoneInfo(key='America/New_York') datetime.timezone(datetime.timedelta(days=-1, seconds=68400))
2026-03-08 09:00:00-04:00
2026-03-08 10:00:00-04:00
1 0

before == after is True, because it's the same instant. What's gone is the zone rule: the restored value carries a fixed -05:00 offset, so "plus one day" no longer knows the clocks move forward that night. fold, which picks between the two 1:30 AMs when DST ends, is reset to 0.

Step 2: Confirm it happens in a real graph

The same thing happens through a compiled graph with a checkpointer:

from typing import TypedDict
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import StateGraph, START, END

class S(TypedDict):
    due: dt.datetime

def noop(state):
    return {}

g = StateGraph(S)
g.add_node("n", noop)
g.add_edge(START, "n")
g.add_edge("n", END)
app = g.compile(checkpointer=InMemorySaver())

cfg = {"configurable": {"thread_id": "t1"}}
app.invoke({"due": before}, cfg)
print(repr(app.get_state(cfg).values["due"].tzinfo))

Output:

datetime.timezone(datetime.timedelta(days=-1, seconds=68400))

Why it happens

Per the issue, the serializer encodes a datetime with isoformat() and decodes it with fromisoformat(). An ISO-8601 string carries the UTC offset in effect at that moment, not the IANA zone name, so the zone can't come back. The issue also points out that datetime.time values in the same file keep their ZoneInfo and fold, and that a bare ZoneInfo round-trips fine.

Step 3: Store the zone name next to the time

Until a fix ships, don't rely on the checkpointer to keep the zone. Keep the zone name as data and rebuild the datetime when you read it. One way:

def pack(d: dt.datetime) -> dict:
    return {"local": d.replace(tzinfo=None).isoformat(), "tz": d.tzinfo.key, "fold": d.fold}

def unpack(p: dict) -> dt.datetime:
    return dt.datetime.fromisoformat(p["local"]).replace(tzinfo=ZoneInfo(p["tz"]), fold=p["fold"])

r = unpack(s.loads_typed(s.dumps_typed(pack(before))))
print(repr(r.tzinfo), (r + dt.timedelta(days=1)).astimezone(ny))

Output:

zoneinfo.ZoneInfo(key='America/New_York') 2026-03-08 09:00:00-04:00

Other options that also avoid the problem:

  • Store UTC plus the user's zone name (for example in a separate state key) and convert to local time only when you need wall-clock math.
  • Re-attach the zone after a resume at the start of the node that does the date math: d.astimezone(ZoneInfo(tz_name)) gives you a ZoneInfo-aware value again, as long as you kept tz_name somewhere.

ZoneInfo.key only exists for zones built from a name. If your code can receive datetime.timezone offsets too, store those as an offset, not a key.

Step 4: Check your existing threads

If you already store aware datetimes in state, any value read back from a checkpoint has a fixed offset. A quick check in a node or a test:

from zoneinfo import ZoneInfo

def has_zone(d: dt.datetime) -> bool:
    return isinstance(d.tzinfo, ZoneInfo)

Times saved before a DST change and used after it are the ones to look at. Only the date arithmetic is affected; comparisons and sorting of instants still work, because the instant is preserved.

Skip the plumbing with MemorySync

Disclosure: I build MemorySync.

This doesn't change the fix above: whatever you keep in checkpointed state still goes through LangGraph's serializer, so store times as in Step 3. The MemorySync LangGraph guide is about a different layer. For create_agent it documents MemorySyncMemoryMiddleware(user_id=...), and for create_react_agent a pre-model hook. It says both "shape only what the model sees for that one call" and that "checkpointed graph state is never mutated". So the checkpointer keeps your thread state, timestamps included, and MemorySync holds what should outlast a thread for that user. Limits from the guide: it requires langgraph 1.2+ in Python and lists langchain-memorysync[langgraph] 1.2.0.

From the guide:

Unverified (not run):

pip install "langchain-memorysync[langgraph]"
from langchain.agents import create_agent
from langchain_memorysync.langgraph import MemorySyncMemoryMiddleware

agent = create_agent(
    model,
    tools=tools,
    middleware=[MemorySyncMemoryMiddleware(user_id="customer-7")],
)

Snippet from the MemorySync LangGraph guide; I haven't run it for this page. Set MEMORYSYNC_API_KEY in the environment, as the guide says.

Try it: MemorySync is optional here: the steps above work without it. If you want the memory layer from the snippet, see MemorySync, create an API key in the dashboard and drop the snippet into your agent.

FAQ

Is the stored instant wrong? No. In my run before == after was True. The zone rule and fold are lost, so later arithmetic in local time can be off by an hour across DST.

Does datetime.timezone.utc have the problem? A fixed offset has no DST rule to lose, so it comes back as the same fixed offset. The issue is specific to real zones such as ZoneInfo("America/New_York").

Will upgrading fix it? Not yet as of Oct 30, 2026: the issue is open and it reproduces on langgraph 1.2.18 / langgraph-checkpoint 4.3.1. Watch #8826 for the merged fix.

Sources

  • github.com/langchain-ai/langgraph/issues/8826 (opened 2026-09-05, open, 10 comments; repro, root cause and fix branch discussion; read 2026-10-30)
  • PyPI release info for langgraph 1.2.18 and langgraph-checkpoint 4.3.1 (read 2026-10-30); latest langgraph 1.2.20 / checkpoint 4.3.1 as of 2026-11-09
  • Offline runs on langgraph 1.2.18 / langgraph-checkpoint 4.3.1, Python 3.13, InMemorySaver (2026-10-30); 1.2.20 not re-run
  • MemorySync LangGraph guide: docs.memorysync.io/guides/langgraph (fetched 2026-10-30)

Try it

For an overview of MemorySync, see the MemorySync homepage.

Next step: read the MemorySync LangGraph guide, then see MemorySync. Founding offer: use code FOUNDING30 for 30% off Starter for your first 3 months (Settings → Billing).

About the author

  • Building MemorySync (memorysync.io) — persistent memory infrastructure for AI agents, Cursor, and Claude Code.

No comments yet. Be the first to comment.