Token Meter

Token Meter / Guides / Slow MCP tools

Which MCP tool is
actually slow?

Claude Code and Codex both stamp every tool call with a latency_ms at ingest time, but nothing rolls that up for you by default — a chronically slow tool just blends into the pace of a session. The audit command's slow_tool detector (D3) groups calls by tool, MCP server, source, and project, and reports mean, p50, p95, and max latency for each group — so you can tell "this tool has a few catastrophic outliers" from "this tool is just slow, every single time."

Updated 2026-07-15 · Uses the token-meter audit command's slow_tool (D3) detector

Problem

A session drags — there's a multi-second pause after a tool call, over and over, and it's slowing down how fast you can iterate. Is it the model thinking? A specific MCP server having a bad day? A built-in tool (Bash, Read) doing something heavy? You want a name and a number — "[playwright] browser_navigate is averaging 4.2 seconds a call" — not a vague sense that "MCP feels slow today." And once you have a name, you want to know whether it's worth fixing at all: a tool that's slow on every call is a different problem than a tool that's fast 95% of the time and occasionally spikes.

Cause

tool_events.latency_ms is captured at parse time by pairing a tool_use block with its matching tool_result and taking the timestamp delta — src/parser.ts for Claude Code, src/codex-parser.ts for Codex (both compute Math.max(0, resultTs - callTs)). Both sources populate it the same way, so this isn't a Claude Code-only signal the way some other audit findings are. But a per-call number sitting in SQLite isn't a finding — nothing groups it, averages it, or tells you when it crosses a line worth caring about.

That's what the slow_tool detector (D3 of the audit's six) does: it groups tool_events by (tool_name, mcp_server, source, project) and flags a group only once it clears two bars together — mean latency ≥3,000ms and ≥5 calls with a recorded latency in the window (SLOW_TOOL_LATENCY_MS_THRESHOLD / SLOW_TOOL_MIN_CALLS in src/audit/config.ts). Four slow calls, or twenty calls averaging 2.9 seconds, produce nothing — not enough evidence either way. That bar is deliberately identical to the older HIGH_LATENCY pattern in trim-suggestions.ts (the Pro auto-trim-suggestions feature), so "slow" means the same 3-second/5-call thing whether you're reading a trim suggestion or an audit finding.

Once a group qualifies, p50 and p95 are fetched directly from SQLite as ORDER BY latency_ms ASC LIMIT 1 OFFSET <rank> (the nearest-rank method) rather than pulling every row for a busy tool into JS and sorting there — one bounded row per percentile no matter how many calls are in the group. Calls with no recorded latency_ms (NULL) are excluded from mean/p50/p95/max entirely — never treated as 0ms — and counted separately as callsMissingLatency, so the exclusion stays visible in the finding's evidence instead of silently shrinking the sample. And failure_rate is always null: this schema has no error/failure/status column on tool_events at all, so the field is reported as "unavailable," never faked as 0%.

Command

Prerequisite: token-meter ingest must have run at least once — audit reads the same local SQLite DB (~/.tokenpulse/usage.db) every other command uses. Nothing below calls out to Anthropic, OpenAI, or any MCP server.

1. Run the audit

npx -y @whdrnr2583/token-meter audit --days 7

--days defaults to 7 and is clamped to your tier's history cap (7d Free, 30d Pro, unlimited Pro+) — a clamp warning prints to stderr if you asked for more than you're entitled to. --limit defaults to 5 in terminal mode, 20 in --json mode.

2. Make sure a slow_tool finding isn't hiding behind cost-bearing ones

slow_tool never carries a dollar figure (costLabel: 'not_available', estimatedCostUsd: null — latency has no direct cost in this schema). The audit engine's cross-detector ranking sorts every cost-bearing finding first, by cost descending, and only then the no-cost findings — so a genuinely bad latency finding can sit past the default terminal --limit 5 behind three cheaper-but-priced findings. Raise the limit, or go straight to JSON and filter by type:

token-meter audit --days 7 --limit 20

# machine-readable — filter to just the latency findings (needs jq)
token-meter audit --days 7 --limit 20 --json \
  | jq '.findings[] | select(.type == "slow_tool")'

3. Scope to one MCP server, source, or project

--source all|claude|codex and --project <value> narrow the underlying query itself for this detector — its GROUP BY already includes source and project, so scoping actually confines which calls feed each group (unlike oversized_tool_response, whose aggregate has no source/project columns and just echoes back whatever you asked for — see the MCP token usage guide for that caveat).

token-meter audit --days 7 --source claude --project /Users/you/repo --json \
  | jq '.findings[] | select(.type == "slow_tool")'

Synthetic output

Fabricated example numbers below — not real user data — to show the shape of each command's output.

token-meter audit --days 14 (terminal, one finding shown — note it's ranked below cost-bearing findings)

5. [playwright] browser_navigate calls averaging 4.2s
Source: claude-code
Project: /Users/example/mcp-agent-demo
Session: -
Tool: browser_navigate
Evidence: Mean latency 4.2s (4,187.6ms) across 42 call(s) with a recorded latency_ms, in the 14d window. p50 (median) 1.1s, p95 18.3s, max 22.0s. failure_rate is not available: tool_events has no error/failure/status column in this schema, so it is reported as null rather than fabricated. 3 additional call(s) to [playwright] browser_navigate in this window had no recorded latency_ms and were excluded from the stats above (not counted, not treated as 0ms).
Confidence: high
Suggested action: Investigate whether [playwright] browser_navigate's output is actually used downstream; if not, drop the call or cache the result to avoid the 4.2s penalty per call.

token-meter audit --days 14 --json | jq '.findings[] | select(.type == "slow_tool")'

{
  "id": "d3a1f70b2c9e4411",
  "rank": 5,
  "type": "slow_tool",
  "title": "[playwright] browser_navigate calls averaging 4.2s",
  "description": "[playwright] browser_navigate averaged 4.2s per call over 42 call(s) in the last 14d (p50 1.1s, p95 18.3s, max 22.0s).",
  "source": "claude-code",
  "project": "/Users/example/mcp-agent-demo",
  "sessionId": null,
  "toolName": "browser_navigate",
  "metrics": {
    "toolName": "browser_navigate",
    "mcpServer": "playwright",
    "meanMs": 4187.6,
    "p50Ms": 1120,
    "p95Ms": 18340,
    "maxMs": 21980,
    "callCount": 42,
    "failureRate": null
  },
  "estimatedCostUsd": null,
  "costLabel": "not_available",
  "confidence": "high",
  "evidence": [
    "Mean latency 4.2s (4,187.6ms) across 42 call(s) with a recorded latency_ms, in the 14d window.",
    "p50 (median) 1.1s, p95 18.3s, max 22.0s.",
    "failure_rate is not available: tool_events has no error/failure/status column in this schema, so it is reported as null rather than fabricated.",
    "3 additional call(s) to [playwright] browser_navigate in this window had no recorded latency_ms and were excluded from the stats above (not counted, not treated as 0ms)."
  ],
  "recommendations": [
    "Investigate whether [playwright] browser_navigate's output is actually used downstream; if not, drop the call or cache the result to avoid the 4.2s penalty per call.",
    "If the latency looks environmental (network, cold start), check the \"playwright\" MCP server's health rather than the calling code first."
  ],
  "costEventIds": []
}
{
  "id": "9f2b6a0d1c7e3355",
  "rank": 6,
  "type": "slow_tool",
  "title": "[notion] notion_search calls averaging 3.8s",
  "description": "[notion] notion_search averaged 3.8s per call over 19 call(s) in the last 14d (p50 3.8s, p95 4.1s, max 4.3s).",
  "source": "claude-code",
  "project": "/Users/example/mcp-agent-demo",
  "sessionId": null,
  "toolName": "notion_search",
  "metrics": {
    "toolName": "notion_search",
    "mcpServer": "notion",
    "meanMs": 3812.4,
    "p50Ms": 3760,
    "p95Ms": 4090,
    "maxMs": 4310,
    "callCount": 19,
    "failureRate": null
  },
  "estimatedCostUsd": null,
  "costLabel": "not_available",
  "confidence": "medium",
  "evidence": [
    "Mean latency 3.8s (3,812.4ms) across 19 call(s) with a recorded latency_ms, in the 14d window.",
    "p50 (median) 3.8s, p95 4.1s, max 4.3s.",
    "failure_rate is not available: tool_events has no error/failure/status column in this schema, so it is reported as null rather than fabricated."
  ],
  "recommendations": [
    "Investigate whether [notion] notion_search's output is actually used downstream; if not, drop the call or cache the result to avoid the 3.8s penalty per call.",
    "If the latency looks environmental (network, cold start), check the \"notion\" MCP server's health rather than the calling code first."
  ],
  "costEventIds": []
}

Interpretation

The two synthetic findings above are the same shape of problem — both cross the 3s/5-call bar — but read very differently once you line up all four numbers:

Confidence reflects sample size only — high at ≥20 calls with a recorded latency, medium below that (the audit's HAVING clause already filters out anything under 5 calls, so nothing below the 5-call floor reaches this metric at all). It says nothing about whether the pattern is outlier-driven or uniform — that judgment is entirely in reading mean against p50/p95/max yourself, the way the two examples above do.

Actions

This is a spotlight, not an accusation (see docs/audit.md) — a slow call that isn't on the critical path, or that ran while other work proceeded, may not have cost you any real time at all. Read the numbers, then decide.

  1. Outlier pattern (mean ≫ p50): don't treat the tool as broken — treat the slow minority as a separate question. Narrow --days to the window right around when the session felt slow, and cross-reference the timestamp against your own Claude Code/Codex transcript for that session (Token Meter never stores call arguments, so it can tell you "this tool+server was slow," never "on this specific query" — see Limitations). Common culprits: the first call of a session (cold start/connection setup), a specific target (one flaky page, one large repo), or a transient network condition.
  2. Uniform pattern (mean ≈ p50 ≈ p95): treat it as a fixed cost, not an anomaly to debug. Options: cache the result if the call is idempotent and repeated, reduce how often it's called (check whether the audit's repeated_similar_tool_calls / D4 finding also fired for the same tool+session), switch to a lighter equivalent tool or narrower query, or accept the round-trip if what it returns is worth the wait.
  3. If mcp_server is set, the finding's own recommendation is to check that server's health (rate limits, region, cold-start behavior) before assuming your own code or prompt is at fault — the latency lives on their side of the call.
  4. If mcp_server is null, the slow tool is one of Claude Code's/Codex's own built-ins (Bash, Read, …) — the latency is local (a slow command, a large file, a slow filesystem), not a remote dependency, so look at what the call was actually doing rather than an MCP server's health.
  5. Ask whether the output is used at all. If a slow tool's result is read once and mostly ignored downstream, dropping the call outright removes both the latency and the tokens it cost — often the cheapest fix available.
  6. Re-run periodically. The 3s/5-call bar is fixed, not adaptive — a tool creeping toward 2,900ms won't show up until it actually crosses the line, so a one-time check doesn't catch a slow regression introduced later.

Limitations

← Token Meter home · Source on GitHub · Full audit docs