anthropics / anthropics/claude-code
[BUG] Individual sessions degrade 10-30x in latency while concurrent sessions on the same project, version and model stay normal
- Langage dominant
- Python
- Étoiles
- 145k
- Forks
- 23.1k
- Métriques de merge des PR
- Métriques de PR en attente
Description
> **This report has been substantially rewritten.** The original version attributed the slowdown to a regression in image handling introduced around 2.1.252. Further testing **disproved that**: downgrading to 2.1.251 did not help (it got slower), and sessions carrying more and larger images than the affected one run at normal speed. The correct finding is below — the degradation is **per-session**, and every structural explanation I tried is ruled out by a direct counterexample. The measurement method and script are unchanged.
### Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report
- [x] I am using the latest version of Claude Code
### What's Wrong?
## Summary
An individual session can degrade to **10–30x** its normal latency and stay that way for its whole lifetime, while **other sessions running at the same time, on the same project, version, model and machine, remain completely normal**. The degraded session never recovers, including across `--resume`.
The affected session (`S69`) averaged **175s per turn**; a session in the *same project* an hour earlier averaged **10 ms/tok**. This is not context size, not images, not session age, not CLI version, and not a server-wide slowdown — each of those is ruled out by a counterexample below.
## Decisive evidence: concurrent sessions, same hour
`S69` is the affected session. All measurements are ms per output token, so generation length is controlled for. Every session below ran on the same machine, same account, same CLI build, same model:
```
09-02 11 S03=16 (n=38, project-3) | S69=118 (n=26, project-14) | S40=18 (n=22, project-7) | S67=10 (n=13, project-14)
09-02 13 S03=17 (n=47, project-3) | S84=11 (n=36, project-1) | S69=225 (n=22, project-14)
09-02 14 S03=22 (n=33, project-3) | S69=322 (n=20, project-14) | S84=16 (n=9, project-1)
09-02 15 S03=26 (n=45, project-3) | S69=310 (n=13, project-14) | S84=13 (n=8, project-1)
```
Note `S67` — a **different session in the same project (project-14)** as `S69`, in the same hour, at **10 ms/tok**. Whatever is wrong is scoped to the session, not the project, the config, the machine, or the backend at that moment.
## Every structural explanation has a counterexample
Ranking all sessions with >=40 turns on the same model:
```
ms/tok turns imgs imgMB peakCtx ageH version session
137.0 179 10 4.4 336,267 16.7 2.1.258 S69 <- affected
85.0 233 0 0.0 386,105 3.8 2.1.233 S04
38.0 604 1 0.3 566,874 21.9 2.1.246 S53
30.8 289 17 5.9 557,319 7.8 2.1.250 S41
27.2 619 0 0.0 562,898 99.3 2.1.237 S09
10.4 440 1 0.0 419,760 170.6 2.1.237 S46
10.1 162 0 0.0 426,841 2.9 2.1.234 S54
```
| Hypothesis | Counterexample |
|---|---|
| Large context | `S46`: 420k peak context, **10.4 ms/tok** |
| Images in context | `S41`: 17 images totalling 5.9MB (vs S69's 10 / 4.4MB), **30.8 ms/tok** — 4x faster |
| Session age | `S46`: 170 hours old, still 10.4 ms/tok |
| Turn count | `S09`: 619 turns, 27.2 ms/tok |
| CLI version | See downgrade test below |
| Backend-wide slowdown | Concurrent sessions in the same hour are normal |
| Project config (hooks / MCP / CLAUDE.md) | `S67`, same project, same hour, 10 ms/tok |
## Downgrading the CLI does not help
I resumed the affected session on 2.1.251 (a build whose sessions were otherwise healthy). It got **worse**, tracking the session's own trajectory rather than the version:
```
2.1.252: n= 69 88.0 ms/tok mean 64.3s mean ctx 110,651
2.1.258: n=102 160.8 ms/tok mean 110.8s mean ctx 240,687
2.1.251: n= 8 248.8 ms/tok mean 221.9s mean ctx 331,468 <- after downgrade
```
## The degradation has a sharp onset
Within the affected session, holding version (2.1.258) and image count (7) fixed:
```
context n ms/tok mean s
200-240k 16 18.6 14.3
240-270k 16 268.9 146.4
270-999k 23 344.4 175.5
```
18.6 -> 268.9 ms/tok across a 25% change in context. For comparison, pooled across all my sessions the same model goes from 16.9 ms/tok (0–50k) to only 29.8 ms/tok (400–600k) — a gentle slope with no cliff. So the cliff is a property of this session, not of context size in general.
## Prompt cache is healthy — this is not #90716
[#90716](https://github.com/anthropics/claude-code/issues/90716) (image eviction rewriting the conversation prefix) shows `cache_read` collapsing to a small constant with `cache_creation` at full context. Not the case here:
```
turns: 179 turns with cache_creation > 50k: 2 (1.1%)
mean cache_read: 190,649 mean cache_creation: 3,968
```
`promptTotal` is monotonically increasing throughout — the prefix is not being rewritten.
## Relation to #71770
[#71770](https://github.com/anthropics/claude-code/issues/71770) reports the same shape — "new session is fast (~10s first byte), same network, same model, same MCP config; worsens over session lifetime; not correlated with context usage" — and was closed with no comment or explanation. This report adds the control that one was missing: **concurrent healthy sessions on the same machine at the same moment**, which rules out the backend-load reading its author suspected.
## Reproduction
I do not have a deterministic trigger, which is the main gap here. What I can say: the onset was sharp, mid-session, and permanent, and it survived a CLI downgrade and multiple resumes. Opening a fresh session in the same project restored normal latency immediately.
This script identifies the pattern from any user's own transcripts. It prints anonymous session ids only — no paths, project names, or account data:
probe.py
```python
#!/usr/bin/env python3
"""Detect per-session latency degradation in Claude Code transcripts.
Prints two tables:
1. Every session ranked by latency per output token.
2. Hour-by-hour comparison of sessions that ran concurrently.
Session and project names are replaced with anonymous ids.
Usage: python3 probe.py [~/.claude/projects]
"""
import json, os, glob, sys
from collections import defaultdict
from datetime import datetime
root = os.path.expanduser(sys.argv[1] if len(sys.argv) > 1 else "~/.claude/projects")
def parse_ts(s):
try: return datetime.fromisoformat(s.replace("Z", "+00:00"))
except Exception: return None
rows = []
for f in glob.glob(os.path.join(root, "*", "*.jsonl")):
proj = os.path.basename(os.path.dirname(f))
prev = prev_type = None
imgs = img_bytes = 0
t0 = None
for line in open(f, errors="ignore"):
try: r = json.loads(line)
except Exception: continue
t = parse_ts(r.get("timestamp", ""))
if t is not None:
if t0 is None: t0 = t
# An 'attachment' record immediately precedes the model's first
# output of a turn, so this delta is TTFT + generation.
if r.get("type") == "assistant" and prev is not None and prev_type == "attachment":
m = r.get("message") or {}
u = m.get("usage") or {}
ctx = (u.get("input_tokens", 0) + u.get("cache_read_input_tokens", 0)
+ u.get("cache_creation_input_tokens", 0))
out = u.get("output_tokens", 0)
dt = (t - prev).total_seconds()
if 0 < dt < 900 and ctx > 0 and out > 0:
rows.append({"dt": dt, "out": out, "ctx": ctx, "imgs": imgs,
"img_mb": img_bytes / 1048576, "age_h": (t - t0).total_seconds() / 3600,
"proj": proj, "sess": f, "model": m.get("model", "?"),
"ver": r.get("version", "?"), "hour": t.strftime("%m-%d %H"),
"cr": u.get("cache_read_input_tokens", 0),
"cc": u.get("cache_creation_input_tokens", 0)})
prev, prev_type = t, r.get("type")
c = (r.get("message") or {}).get("content")
if isinstance(c, list):
for b in c:
if isinstance(b, dict) and b.get("type") == "tool_result" and isinstance(b.get("content"), list):
for x in b["content"]:
if isinstance(x, dict) and x.get("type") == "image":
imgs += 1
img_bytes += len(x.get("source", {}).get("data", ""))
if not rows:
sys.exit("no turns found")
PM = {p: f"project-{i+1}" for i, p in enumerate(sorted({r["proj"] for r in rows}))}
SM = {s: f"S{i+1:02d}" for i, s in enumerate(sorted({r["sess"] for r in rows}))}
for r in rows:
r["P"], r["S"] = PM[r["proj"]], SM[r["sess"]]
def ms(v): return sum(r["dt"] for r in v) / sum(r["out"] for r in v) * 1000
by_sess = defaultdict(list)
for r in rows: by_sess[r["S"]].append(r)
print("Sessions ranked by latency per output token (>=40 turns)")
print(f"{'ms/tok':>8} {'turns':>6} {'imgs':>5} {'imgMB':>6} {'peakCtx':>9} {'ageH':>7} {'version':>9} session")
ranked = []
for s, v in by_sess.items():
if len(v) >= 40:
ranked.append((ms(v), len(v), max(r["imgs"] for r in v), max(r["img_mb"] for r in v),
max(r["ctx"] for r in v), max(r["age_h"] for r in v),
max(set(r["ver"] for r in v), key=lambda x: sum(1 for r in v if r["ver"] == x)), s))
ranked.sort(reverse=True)
for a in ranked:
print(f"{a[0]:8.1f} {a[1]:6} {a[2]:5} {a[3]:6.1f} {a[4]:9,} {a[5]:7.1f} {a[6]:>9} {a[7]}")
print("\nConcurrent sessions, by hour (only hours with 2+ active sessions)")
by_hour = defaultdict(list)
for r in rows: by_hour[r["hour"]].append(r)
for h in sorted(by_hour):
bs = defaultdict(list)
for r in by_hour[h]: bs[r["S"]].append(r)
parts = [f"{s}={ms(g):.0f} (n={len(g)}, {g[0]['P']})"
for s, g in sorted(bs.items(), key=lambda x: -len(x[1])) if len(g) >= 5]
if len(parts) >= 2:
print(f" {h} " + " | ".join(parts))
```
If a degraded session's turns can be correlated server-side with a request id, that would probably be more informative than anything further I can measure from the client.
### What Should Happen?
A session's latency should not permanently diverge by 10–30x from concurrent sessions doing comparable work. Failing that, it should be **observable**: right now the only signal is the wall clock, and the only remedy anyone can find is "start a new session", which costs the accumulated context.
### Environment
- Claude Code 2.1.258 (also reproduced on 2.1.251 and 2.1.252)
- macOS, Apple Silicon
- Model: `claude-opus-5[1m]`
- Measurements from local `~/.claude/projects/*.jsonl` transcripts, 13k+ turns
Guide de contribution
Aucun guide de contribution indexé pour ce dépôt
Piste de recherche
Start with the embedded probe.py and local ~/.claude/projects/*.jsonl transcripts; run python3 probe.py [~/.claude/projects] to verify the per-session latency pattern and inspect its context, cache, version, and concurrency fields. No repository source file or deterministic trigger is named, so progress requires identifying a reproducible cause or diagnostic path. Done means preventing permanent 10–30x divergence or exposing a useful signal when it occurs.
Rédigé par le modèle d'indexation à partir du texte de l'issue.
Évaluation
- Stack technique
- python
- Domaine
- observability, performance
- Type d'issue
- Bug
- Difficulté
- 5/5
- Temps estimé
- Plus d'une semaine
- Activité
- Active
- Clarté
- Plutôt claire
- Accessibilité débutants
- 25/100