GPT-5.5 is very slow (tokens/s) on a Pro 20x sub through the Codex endpoint compared to before

Resolved 💬 2 comments Opened Jun 29, 2026 by hourianto Closed Jun 30, 2026

What issue are you seeing?

I know in general API gets preference for faster use, but in the last ~week it has become completely unbearable. I have a simple tool to compare the tok/s speed, and right now I see these values for a prompt with gpt 5.5 effort none that tells it to generate ~500 words:

API normal 33.42
API priority 37.99 (1.14x)
Sub normal 16.42 (0.49x)

In the past the sub tok/s was closer to ~30-35 tok/s, so right now it's basically 2x slower, which is way worse. Is this known, and if so, when will it be fixed?

I know that network/etc can play a part in this, but I've verified from multiple different devices, VPSes in different countries. And as yet another datapoint: using websockets also does not change this. GPT 5.5 is also visibly slow in Codex, it's clearly struggling, and honestly such speed is really unusable and not interactive at all. It can take a minute for it towrite

What steps can reproduce the bug?

<details>

<summary>Node snippet to check your current speed with SSE with auth from ~/.codex/auth.json</summary>

#!/usr/bin/env node

const os = await import("node:os");
const fs = await import("node:fs/promises");

const label = `${os.hostname()}-${new Date().toISOString()}`;
const model = "gpt-5.5";
const prompt = "Write exactly 600 words about a library. Use simple prose and no markdown.";
const authPath = `${os.homedir()}/.codex/auth.json`;

const authRaw = await fs.readFile(authPath, "utf8");
const auth = JSON.parse(authRaw);
const token = auth?.tokens?.access_token;
const accountId = auth?.tokens?.account_id ||
  decodeIdTokenAccount(auth?.tokens?.id_token);
if (!token) throw new Error(`${authPath} missing tokens.access_token`);

const body = {
  model,
  store: false,
  stream: true,
  instructions: "",
  input: [{ role: "user", content: prompt }],
  reasoning: { effort: "none" },
};

const started = performance.now();
const headers = {
  "Content-Type": "application/json",
  "Authorization": `Bearer ${token}`,
  "User-Agent": "codex-cli",
};
if (accountId) headers["ChatGPT-Account-ID"] = accountId;

const response = await fetch("https://chatgpt.com/backend-api/codex/responses", {
  method: "POST",
  headers,
  body: JSON.stringify(body),
});

const headersMs = performance.now() - started;
const selectedHeaders = {};
for (const key of [
  "cf-ray",
  "openai-processing-ms",
  "x-request-id",
  "x-envoy-upstream-service-time",
  "server-timing",
]) {
  const value = response.headers.get(key);
  if (value) selectedHeaders[key] = value;
}

if (!response.ok || !response.body) {
  console.log(JSON.stringify({
    label,
    model,
    ok: false,
    status: response.status,
    headersMs: Math.round(headersMs),
    selectedHeaders,
    errorBody: (await response.text()).slice(0, 500),
  }));
  process.exit(0);
}

const decoder = new TextDecoder();
let buffer = "";
let firstChunkMs;
let firstEventMs;
let createdMs;
let firstDeltaMs;
let completedMs;
let eventCount = 0;
let deltaCount = 0;
let totalBytes = 0;
let outputChars = 0;
let usage = {};
let lastDeltaMs;
const deltaGaps = [];
const eventTypes = {};

for await (const chunk of response.body) {
  const now = performance.now() - started;
  if (firstChunkMs === undefined) firstChunkMs = now;
  totalBytes += chunk.byteLength;
  buffer += decoder.decode(chunk, { stream: true }).replaceAll("\r", "");
  drain();
}
buffer += decoder.decode().replaceAll("\r", "");
if (buffer.trim()) processBlock(buffer);

const elapsed = completedMs ?? (performance.now() - started);
const sortedGaps = [...deltaGaps].sort((a, b) => a - b);
console.log(JSON.stringify({
  label,
  model,
  ok: true,
  status: response.status,
  headersMs: round(headersMs),
  firstChunkMs: round(firstChunkMs),
  firstEventMs: round(firstEventMs),
  createdMs: round(createdMs),
  firstDeltaMs: round(firstDeltaMs),
  createdToDeltaMs: createdMs !== undefined && firstDeltaMs !== undefined
    ? round(firstDeltaMs - createdMs)
    : null,
  completedMs: round(elapsed),
  eventCount,
  deltaCount,
  outputChars,
  totalBytes,
  selectedHeaders,
  eventTypes,
  deltaGapMs: {
    avg: sortedGaps.length ? round(mean(sortedGaps)) : null,
    p50: quantile(sortedGaps, 0.50),
    p90: quantile(sortedGaps, 0.90),
    p99: quantile(sortedGaps, 0.99),
    max: sortedGaps.length ? round(sortedGaps[sortedGaps.length - 1]) : null,
  },
  usage,
  outputTokensPerSec: usage.output_tokens && elapsed
    ? Number((usage.output_tokens / (elapsed / 1000)).toFixed(2))
    : null,
  outputCharsPerSec: outputChars && elapsed
    ? Number((outputChars / (elapsed / 1000)).toFixed(2))
    : null,
}));

function drain() {
  while (true) {
    const splitIndex = buffer.indexOf("\n\n");
    if (splitIndex < 0) return;
    const block = buffer.slice(0, splitIndex);
    buffer = buffer.slice(splitIndex + 2);
    processBlock(block);
  }
}

function processBlock(block) {
  const payload = block.split("\n")
    .filter((line) => line.startsWith("data:"))
    .map((line) => line.slice(5).trimStart())
    .join("\n");
  if (!payload || payload === "[DONE]") return;
  const now = performance.now() - started;
  eventCount++;
  if (firstEventMs === undefined) firstEventMs = now;
  let event;
  try {
    event = JSON.parse(payload);
  } catch {
    return;
  }
  const type = event?.type || "unknown";
  eventTypes[type] = (eventTypes[type] || 0) + 1;
  if (type === "response.created" && createdMs === undefined) createdMs = now;
  if (
    type === "response.output_text.delta" ||
    type === "response.refusal.delta" ||
    type === "response.reasoning_summary_text.delta"
  ) {
    deltaCount++;
    const text = typeof event.delta === "string" ? event.delta : "";
    outputChars += text.length;
    if (firstDeltaMs === undefined) firstDeltaMs = now;
    if (lastDeltaMs !== undefined) deltaGaps.push(now - lastDeltaMs);
    lastDeltaMs = now;
  }
  if (type === "response.completed") {
    completedMs = now;
    usage = event?.response?.usage || {};
  }
}

function decodeIdTokenAccount(idToken) {
  const part = idToken?.split(".")?.[1];
  if (!part) return undefined;
  const padded = part.replaceAll("-", "+").replaceAll("_", "/")
    .padEnd(Math.ceil(part.length / 4) * 4, "=");
  try {
    const claims = JSON.parse(Buffer.from(padded, "base64").toString("utf8"));
    return claims?.["https://api.openai.com/auth"]?.chatgpt_account_id;
  } catch {
    return undefined;
  }
}

function mean(values) {
  return values.reduce((sum, value) => sum + value, 0) / values.length;
}

function quantile(values, q) {
  if (!values.length) return null;
  return round(values[Math.floor((values.length - 1) * q)]);
}

function round(value) {
  return value === undefined ? null : Math.round(value);
}

</details>

What is the expected behavior?

Decent tok/s performance, at least steady ~30-35 tok/s on a subscription.

Additional information

Thread ID from feedback: 019f1187-19c9-7230-a602-bbf6aa8b9f6a

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗