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

*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](https://github.com/langchain-ai/langgraph/issues/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](https://docs.memorysync.io/guides/langgraph?utm_source=hashnode&utm_medium=organic_page&utm_campaign=v5_oct25&utm_content=pseo_langgraph-checkpoint-datetime-zoneinfo-lost-dst-off-by-one-hour) 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](https://memorysync.io/?utm_source=hashnode&utm_medium=organic_page&utm_campaign=v5_oct25&utm_content=pseo_langgraph-checkpoint-datetime-zoneinfo-lost-dst-off-by-one-hour). Founding offer: use code FOUNDING30 for 30% off Starter for your first 3 months (Settings → Billing).

## Status (Nov 9, 2026)

- [#8826](https://github.com/langchain-ai/langgraph/issues/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:

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

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

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

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

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

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

```python
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](https://docs.memorysync.io/guides/langgraph?utm_source=hashnode&utm_medium=organic_page&utm_campaign=v5_oct25&utm_content=pseo_langgraph-checkpoint-datetime-zoneinfo-lost-dst-off-by-one-hour) 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):**

```bash
pip install "langchain-memorysync[langgraph]"
```

```python
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](https://docs.memorysync.io/guides/langgraph?utm_source=hashnode&utm_medium=organic_page&utm_campaign=v5_oct25&utm_content=pseo_langgraph-checkpoint-datetime-zoneinfo-lost-dst-off-by-one-hour); 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](https://memorysync.io/?utm_source=hashnode&utm_medium=organic_page&utm_campaign=v5_oct25&utm_content=pseo_langgraph-checkpoint-datetime-zoneinfo-lost-dst-off-by-one-hour), 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](https://memorysync.io/?utm_source=hashnode&utm_medium=organic_page&utm_campaign=v5_oct25&utm_content=pseo_langgraph-checkpoint-datetime-zoneinfo-lost-dst-off-by-one-hour).

**Next step:** read 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-checkpoint-datetime-zoneinfo-lost-dst-off-by-one-hour), then [see MemorySync](https://memorysync.io/?utm_source=hashnode&utm_medium=organic_page&utm_campaign=v5_oct25&utm_content=pseo_langgraph-checkpoint-datetime-zoneinfo-lost-dst-off-by-one-hour). Founding offer: use code FOUNDING30 for 30% off Starter for your first 3 months (Settings → Billing).

