{"slug": "how-fast-do-your-claude-code-agents-run-and-who-is-the-session-waiting-on-reads", "title": "How fast do your Claude Code agents run, and who is the session waiting on? Reads your local Claude Code logs, prints speed and time shares. Nothing leaves your machine.", "summary": "A developer released a Python script that parses local Claude Code session logs (~/.claude/projects/*/*.jsonl) to report how many responses ran in standard versus fast mode, the median end-to-end output speed in tokens per second, and the share of time spent by the model, tools and the human across sessions. The tool reads only timestamps, token counts and the speed field, never message content, and keeps all data on the local machine. It caps tool waits at 5 minutes and human gaps at 1 hour, and computes speed only on responses of 200 or more output tokens.", "body_md": "|  | #!/usr/bin/env python3 | \n|  | \"\"\"How fast do your Claude Code agents run, and who is the session waiting on? | \n|  | Reads your local Claude Code session logs (~/.claude/projects/*/*.jsonl) and prints: | \n|  | - how many responses ran at each speed setting (standard vs fast mode) | \n|  | - the median end-to-end output speed of your responses, in tokens per second | \n|  | - while the agent works on its own: the model's share of the time vs the tools' | \n|  | - across whole sessions: the model's, the tools' and your own share of the clock | \n|  | It reads only timestamps, token counts and the speed field. It never prints or | \n|  | sends message content. Nothing leaves your machine. | \n|  | Usage: | \n|  | python3 claude_code_speed.py # last 30 days, every project | \n|  | python3 claude_code_speed.py 7 # last 7 days | \n|  | python3 claude_code_speed.py 30 ~/.claude/projects/<one-project-dir> | \n|  | How the times are measured, so you can judge the numbers: | \n|  | - Model time: from the message the model answered (your prompt or a tool | \n|  | result) to the model's last chunk of that answer. It includes the wait for | \n|  | the first token, so the speed is end to end, like Artificial Analysis's. | \n|  | - Tool time: from the model's tool call to the tool's result. Waits over | \n|  | 5 minutes are left out: those are permission prompts or background jobs. | \n|  | - Your time: from the model's answer to your next message, each gap capped at | \n|  | 1 hour, so a night away from the keyboard does not count as thinking. | \n|  | - Speed is computed on responses of 200 output tokens or more, where the | \n|  | first-token wait matters least. Claude Code sends long contexts, so this | \n|  | end-to-end speed runs below the streaming speed a benchmark reports. | \n|  | - Output tokens include thinking tokens. | \n|  | - Resumed and forked sessions copy earlier history into a new log; each | \n|  | message is counted once. | \n|  | - Dollars are what the tokens would cost at Claude Opus 5.5 list prices, | \n|  | whatever model or plan you actually used. Fast mode doubles every price. | \n|  | \"\"\" | \n|  | import glob | \n|  | import json | \n|  | import os | \n|  | import sys | \n|  | import time | \n|  | from datetime import datetime | \n|  | TOOL_CAP = 300.0 | \n|  | # Claude Opus 5.5 list prices, dollars per million tokens, from | \n|  | # https://platform.claude.com/docs/en/about-claude/pricing (fetched 2026-11-10): | \n|  | # input, cache read, 5-minute cache write, 1-hour cache write, output. | \n|  | # Fast mode doubles every one of them (https://platform.claude.com/docs/en/build-with-claude/fast-mode). | \n|  | PRICE = (4.00, 0.20, 5.00, 8.00, 20.00) | \n|  | HUMAN_CAP = 3600.0 | \n|  | def ts(s): | \n|  | return datetime.fromisoformat(s.replace(\"Z\", \"+00:00\")).timestamp() | \n|  | SEEN = set() | \n|  | SEEN_REQ = set() | \n|  | def events(path): | \n|  | out = [] | \n|  | for line in open(path, errors=\"ignore\"): | \n|  | try: | \n|  | d = json.loads(line) | \n|  | except ValueError: | \n|  | continue | \n|  | if d.get(\"isSidechain\") or \"timestamp\" not in d: | \n|  | continue | \n|  | uid = d.get(\"uuid\") | \n|  | if uid in SEEN: | \n|  | continue # a resumed or forked session repeats earlier history | \n|  | if uid: | \n|  | SEEN.add(uid) | \n|  | kind = d.get(\"type\") | \n|  | msg = d.get(\"message\") or {} | \n|  | if kind == \"user\": | \n|  | c = msg.get(\"content\") | \n|  | if isinstance(c, list) and any(isinstance(x, dict) and x.get(\"type\") == \"tool_result\" for x in c): | \n|  | who = \"tool_result\" | \n|  | elif d.get(\"isMeta\") or (isinstance(c, str) and c.startswith(\"<\")): | \n|  | who = \"meta\" | \n|  | else: | \n|  | who = \"human\" | \n|  | out.append((ts(d[\"timestamp\"]), \"user\", who, 0, None)) | \n|  | elif kind == \"assistant\": | \n|  | u = msg.get(\"usage\") or {} | \n|  | cc = u.get(\"cache_creation\") or {} | \n|  | w1h = cc.get(\"ephemeral_1h_input_tokens\") or 0 | \n|  | w5m = (u.get(\"cache_creation_input_tokens\") or 0) - w1h | \n|  | use = (u.get(\"input_tokens\") or 0, u.get(\"cache_read_input_tokens\") or 0, w5m, w1h) | \n|  | ctx = sum(use) | \n|  | out.append((ts(d[\"timestamp\"]), \"assistant\", d.get(\"requestId\"), | \n|  | u.get(\"output_tokens\") or 0, (u.get(\"speed\"), ctx, use))) | \n|  | out.sort(key=lambda e: e[0]) | \n|  | return out | \n|  | def main(): | \n|  | days = int(sys.argv[1]) if len(sys.argv) > 1 else 30 | \n|  | root = os.path.expanduser(sys.argv[2]) if len(sys.argv) > 2 else os.path.expanduser(\"~/.claude/projects/*\") | \n|  | cutoff = time.time() - days * 86400 | \n|  | files = sorted((f for f in glob.glob(os.path.join(root, \"*.jsonl\")) if os.path.getmtime(f) >= cutoff), | \n|  | key=os.path.getmtime) | \n|  | model = tool = human = 0.0 | \n|  | sessions = responses = 0 | \n|  | speed_setting, speeds, contexts, out_tokens = {}, [], [], 0 | \n|  | tokens_in = [0, 0, 0, 0] # uncached input, cache reads, 5-minute cache writes, 1-hour cache writes | \n|  | for f in files: | \n|  | ev = events(f) | \n|  | if not ev: | \n|  | continue | \n|  | sessions += 1 | \n|  | prev, i = None, 0 | \n|  | while i < len(ev): | \n|  | t, kind, key, _, _ = ev[i] | \n|  | if kind == \"user\": | \n|  | if prev is not None and t >= prev: | \n|  | gap = t - prev | \n|  | if key == \"tool_result\" and gap <= TOOL_CAP: | \n|  | tool += gap | \n|  | elif key == \"human\": | \n|  | human += min(gap, HUMAN_CAP) | \n|  | prev, i = t, i + 1 | \n|  | continue | \n|  | j, out, (setting, ctx, use) = i, 0, ev[i][4] | \n|  | while j < len(ev) and ev[j][1] == \"assistant\" and ev[j][2] == key: | \n|  | out = max(out, ev[j][3]) | \n|  | j += 1 | \n|  | end = ev[j - 1][0] | \n|  | first = key not in SEEN_REQ # one request can be logged in more than one piece | \n|  | SEEN_REQ.add(key) | \n|  | if prev is not None and 0 < end - prev < 1800: | \n|  | model += end - prev | \n|  | if first: | \n|  | responses += 1 | \n|  | contexts.append(ctx) | \n|  | if out >= 200: | \n|  | speeds.append(out / (end - prev)) | \n|  | if first: | \n|  | out_tokens += out | \n|  | for k in range(4): | \n|  | tokens_in[k] += use[k] | \n|  | if setting and first: | \n|  | speed_setting[setting] = speed_setting.get(setting, 0) + 1 | \n|  | prev, i = end, j | \n|  | if not responses: | \n|  | print(\"No sessions found. Check the path:\", root) | \n|  | return | \n|  | speeds.sort() | \n|  | contexts.sort() | \n|  | q = lambda p: speeds[int(p * (len(speeds) - 1))] | \n|  | clock = model + tool + human | \n|  | print(f\"Last {days} days: {sessions} sessions, {responses} model responses\") | \n|  | print(\"Speed setting recorded:\", \", \".join(f\"{k} {v}\" for k, v in sorted(speed_setting.items())) or \"none\") | \n|  | print(f\"Median output speed: {q(0.5):.0f} tok/s (middle half {q(0.25):.0f} to {q(0.75):.0f}), \" | \n|  | f\"over {len(speeds)} responses of 200+ tokens\") | \n|  | print(f\"While the agent works: model {100 * model / (model + tool):.0f}% of the time, \" | \n|  | f\"tools {100 * tool / (model + tool):.0f}%\") | \n|  | print(f\"Across whole sessions: you {100 * human / clock:.0f}%, model {100 * model / clock:.0f}%, \" | \n|  | f\"tools {100 * tool / clock:.0f}%\") | \n|  | print(f\"Model time: {model / 3600:.0f} hours, {out_tokens / 1e6:.0f}M output tokens, \" | \n|  | f\"median context {contexts[len(contexts) // 2] / 1000:.0f}K tokens per response\") | \n|  | cost = (sum(t * pr for t, pr in zip(tokens_in, PRICE[:4])) + out_tokens * PRICE[4]) / 1e6 | \n|  | print(f\"At Claude Opus 5.5 list prices: ${cost:,.0f} on the standard tier \" | \n|  | f\"(${out_tokens * PRICE[4] / 1e6:,.0f} of it output), ${2 * cost:,.0f} on fast mode\") | \n|  | m_share, a_share = model / clock, model / (model + tool) | \n|  | print(f\"A model 2.5x faster would save about {100 * m_share * 0.6:.0f}% of your session clock, \" | \n|  | f\"and about {100 * a_share * 0.6:.0f}% of an agent's solo run time, \" | \n|  | f\"at most {model * 0.6 / 3600:.0f} hours of model time (fast mode speeds up streaming, \" | \n|  | f\"not the wait for the first token, so read these as ceilings)\") | \n|  | if __name__ == \"__main__\": | \n|  | main() |", "url": "https://wpnews.pro/news/how-fast-do-your-claude-code-agents-run-and-who-is-the-session-waiting-on-reads", "canonical_source": "https://gist.github.com/paolo-perrone/febe1ced3ec28ecb6a9432938efd68b0", "published_at": "2026-11-11 20:16:21+00:00", "updated_at": "2026-11-12 18:55:06.053282+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools", "mlops"], "entities": ["Claude Code", "Anthropic", "Artificial Analysis"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/how-fast-do-your-claude-code-agents-run-and-who-is-the-session-waiting-on-reads", "markdown": "https://wpnews.pro/news/how-fast-do-your-claude-code-agents-run-and-who-is-the-session-waiting-on-reads.md", "text": "https://wpnews.pro/news/how-fast-do-your-claude-code-agents-run-and-who-is-the-session-waiting-on-reads.txt", "jsonld": "https://wpnews.pro/news/how-fast-do-your-claude-code-agents-run-and-who-is-the-session-waiting-on-reads.jsonld"}}