Identical concurrent GPT-5.6 request bodies can split between cache hit and zero

Open 💬 0 comments Opened Jul 17, 2026 by pandysp

What issue are you seeing?

The ChatGPT Codex Responses WebSocket endpoint intermittently returns different prompt-cache results for two simultaneous byte-identical GPT-5.6 response.create bodies. Both use the same session-id and prompt_cache_key. One reports 8,960 cached tokens. The other reports cached_tokens=0.

Node v26.3.0; gpt-5.6-luna/xhigh; up to 12 attempts
 ...
 5 at=2026-07-17T12:26:10.183Z session=019f700a-9787-7ce2-bb6b-d19a84e77fb0 writer=0 readers=0/0
 6 at=2026-07-17T12:26:14.833Z session=019f700a-a9b1-740f-a68e-3f6e85cfec69 writer=0 readers=0/8960  <-- CACHE SPLIT
   responses=resp_010db8f0a8427610016a5a1f67b1e4819199dffad847cfc6a9/resp_06aba322e65af54e016a5a1f69cfe081919c18790679273785/resp_033f6bba16f0fcd2016a5a1f69b4a0819196690ca714960887
FAIL: identical simultaneous readers received different cache results.

The failure condition requires one reader to hit and the other to report zero. An attempt where both readers miss does not count.

What steps can reproduce the bug?

Save the dependency-free reproducer included below as codex-cache-split-mre.mjs. Run it with Node.js 26 and a current ChatGPT OAuth access token:

OPENAI_CODEX_ACCESS_TOKEN='<token>' ATTEMPTS=12 node codex-cache-split-mre.mjs

The script uses Node's built-in WebSocket client against wss://chatgpt.com/backend-api/codex/responses.

For each attempt it:

  1. Generates a fresh UUIDv7 and uses it for both the session-id header and prompt_cache_key.
  2. Sends a large writer request and waits for completion.
  3. Sends two byte-identical reader bodies concurrently. One reuses the writer's WebSocket and one uses a fresh WebSocket. Each physical connection has its own x-client-request-id.
  4. Reports a failure only when one reader gets cached_tokens=0 and the other gets a hit.

The failure is intermittent. A clean 12-attempt run does not rule it out.

<details>
<summary>codex-cache-split-mre.mjs</summary>

#!/usr/bin/env node

// Dependency-free reproducer for inconsistent prompt-cache routing on the
// ChatGPT Codex Responses WebSocket endpoint. Tested with Node.js 26.3.0.

const ENDPOINT = "wss://chatgpt.com/backend-api/codex/responses";
const MODEL = process.env.MODEL || "gpt-5.6-luna";
const EFFORT = process.env.EFFORT || "xhigh";
const ATTEMPTS = Number(process.env.ATTEMPTS || 12);
const ACCESS_TOKEN = process.env.OPENAI_CODEX_ACCESS_TOKEN;

if (!ACCESS_TOKEN) {
  console.error("Set OPENAI_CODEX_ACCESS_TOKEN to a current ChatGPT OAuth access token.");
  process.exit(2);
}
if (typeof WebSocket !== "function") {
  console.error("This script requires a Node.js release with the built-in WebSocket client.");
  process.exit(2);
}

function jwtPayload(token) {
  try {
    return JSON.parse(Buffer.from(token.split(".")[1], "base64url"));
  } catch {
    throw new Error("OPENAI_CODEX_ACCESS_TOKEN is not a JWT access token");
  }
}

const tokenPayload = jwtPayload(ACCESS_TOKEN);
const ACCOUNT_ID =
  process.env.OPENAI_CODEX_ACCOUNT_ID ||
  tokenPayload["https://api.openai.com/auth"]?.chatgpt_account_id;

if (!ACCOUNT_ID) {
  console.error("Could not extract the ChatGPT account id from the token.");
  process.exit(2);
}
if (tokenPayload.exp * 1000 <= Date.now()) {
  console.error("OPENAI_CODEX_ACCESS_TOKEN has expired.");
  process.exit(2);
}

function uuidV7() {
  const bytes = crypto.getRandomValues(new Uint8Array(16));
  let timestamp = Date.now();
  for (let index = 5; index >= 0; index -= 1) {
    bytes[index] = timestamp & 0xff;
    timestamp = Math.floor(timestamp / 256);
  }
  bytes[6] = 0x70 | (bytes[6] & 0x0f);
  bytes[8] = 0x80 | (bytes[8] & 0x3f);
  const hex = Buffer.from(bytes).toString("hex");
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
}

function decodeWebSocketData(data) {
  if (typeof data === "string") return Promise.resolve(data);
  if (data instanceof ArrayBuffer) return Promise.resolve(Buffer.from(data).toString());
  if (ArrayBuffer.isView(data)) {
    return Promise.resolve(Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString());
  }
  if (typeof data?.text === "function") return data.text();
  return Promise.resolve(String(data));
}

function connect(sessionId) {
  const headers = {
    Authorization: `Bearer ${ACCESS_TOKEN}`,
    "chatgpt-account-id": ACCOUNT_ID,
    originator: "pi",
    "User-Agent": `codex-cache-split-mre (${process.platform}; ${process.arch})`,
    "session-id": sessionId,
    "x-client-request-id": uuidV7(),
  };

  return new Promise((resolve, reject) => {
    const socket = new WebSocket(ENDPOINT, { headers });
    const timeout = setTimeout(() => reject(new Error("WebSocket connect timeout")), 15_000);
    socket.addEventListener("open", () => {
      clearTimeout(timeout);
      resolve(socket);
    }, { once: true });
    socket.addEventListener("error", (event) => {
      clearTimeout(timeout);
      reject(new Error(event.message || "WebSocket connection failed"));
    }, { once: true });
  });
}

function request(socket, body) {
  return new Promise((resolve, reject) => {
    const timeout = setTimeout(() => finish(new Error("Response timeout")), 60_000);

    const finish = (error, response) => {
      clearTimeout(timeout);
      socket.removeEventListener("message", onMessage);
      socket.removeEventListener("error", onError);
      socket.removeEventListener("close", onClose);
      if (error) reject(error);
      else resolve(response);
    };
    const onError = (event) => finish(new Error(event.message || "WebSocket error"));
    const onClose = (event) => finish(new Error(`WebSocket closed (${event.code}): ${event.reason}`));
    const onMessage = async (event) => {
      try {
        const message = JSON.parse(await decodeWebSocketData(event.data));
        if (message.type === "response.completed") {
          finish(null, { id: message.response.id, usage: message.response.usage });
        }
        if (message.type === "response.failed") {
          finish(new Error(JSON.stringify(message.response?.error || message)));
        }
        if (message.type === "error") finish(new Error(JSON.stringify(message.error || message)));
      } catch (error) {
        finish(error);
      }
    };

    socket.addEventListener("message", onMessage);
    socket.addEventListener("error", onError);
    socket.addEventListener("close", onClose);
    socket.send(JSON.stringify({ type: "response.create", ...body }));
  });
}

function requestBody(sessionId, input) {
  return {
    model: MODEL,
    store: false,
    stream: true,
    instructions: "Follow the final instruction exactly.",
    input,
    text: { verbosity: "low" },
    include: ["reasoning.encrypted_content"],
    prompt_cache_key: sessionId,
    tool_choice: "auto",
    parallel_tool_calls: true,
    reasoning: { effort: EFFORT, summary: "auto" },
  };
}

function inputText(text) {
  return { role: "user", content: [{ type: "input_text", text }] };
}

function cachedTokens(usage) {
  return usage?.input_tokens_details?.cached_tokens ?? -1;
}

let reproduced = false;
console.log(`Node ${process.version}; ${MODEL}/${EFFORT}; up to ${ATTEMPTS} attempts`);

for (let attempt = 1; attempt <= ATTEMPTS; attempt += 1) {
  const startedAt = new Date().toISOString();
  const sessionId = uuidV7();
  const prefix = [
    `Unique cache probe ${sessionId}.`,
    ...Array.from(
      { length: 600 },
      (_, index) => `Record ${index}: alpha bravo charlie delta echo foxtrot golf hotel.`,
    ),
    "Reply exactly: writer",
  ].join("\n");
  const identicalInput = [inputText(prefix)];

  const primary = await connect(sessionId);
  const writerResponse = await request(primary, requestBody(sessionId, identicalInput));

  // Reuse the writer socket for one reader and use a fresh socket for the other.
  // Both readers have the same session id, cache key, and byte-identical body;
  // each physical connection has its own client request id.
  const overflow = await connect(sessionId);
  const [primaryResponse, overflowResponse] = await Promise.all([
    request(primary, requestBody(sessionId, identicalInput)),
    request(overflow, requestBody(sessionId, identicalInput)),
  ]);

  primary.close();
  overflow.close();

  const writer = cachedTokens(writerResponse.usage);
  const readerA = cachedTokens(primaryResponse.usage);
  const readerB = cachedTokens(overflowResponse.usage);
  const split = (readerA === 0) !== (readerB === 0);
  console.log(
    `${String(attempt).padStart(2)} at=${startedAt} session=${sessionId} writer=${writer} readers=${readerA}/${readerB}${split ? "  <-- CACHE SPLIT" : ""}`,
  );
  console.log(
    `   responses=${writerResponse.id}/${primaryResponse.id}/${overflowResponse.id}`,
  );

  if (split) {
    reproduced = true;
    break;
  }
}

if (reproduced) {
  console.error("FAIL: identical simultaneous readers received different cache results.");
  process.exit(1);
}

console.log("Not reproduced in this run; increase ATTEMPTS and retry (the failure is intermittent).");

</details>

What is the expected behavior?

After the writer has completed, two byte-identical request bodies with the same session-id and prompt_cache_key should not get different cache results based on which physical WebSocket carries them.

Additional information

Environment for the output above:

  • macOS Darwin 25.5.0 arm64
  • Node.js 26.3.0
  • ChatGPT subscription authentication
  • gpt-5.6-luna with reasoning effort xhigh
  • originator: pi
  • full-input WebSocket requests

The values above come directly from response.completed.response.usage.input_tokens_details.cached_tokens.

Related reports:

  • #20301 includes a same-thread sequence with stable cache-relevant request metadata that goes from 26,496 cached tokens to 3,456 and immediately back to 30,080. That report uses GPT-5.5 and sequential incremental requests. This reproducer uses GPT-5.6 and makes simultaneous identical bodies disagree.

View original on GitHub ↗