●OPUS48 — Claude Opus 4.8 is generally available, improving on 4.7 across coding, agentic skills, reasoning, and knowledge work●AUTO — Claude Code now defaults to auto mode on Bedrock, Vertex AI, and Foundry, and updates Bedrock to Opus 4.8●GUARD — Auto mode now blocks tampering with session transcripts and asks before running rm -rf on an unresolved variable●IDP — Admins can provision MCP connectors org-wide via their IdP (starting with Okta), granting access on first login●TIMEOUT — Fixed per-server request_timeout_ms being ignored, which timed out long MCP tool calls at the 60s default●FAST — Fast mode for Opus 4.7 is deprecated and will be removed on July 24; migrate to fast mode for Opus 4.8●OPUS48 — Claude Opus 4.8 is generally available, improving on 4.7 across coding, agentic skills, reasoning, and knowledge work●AUTO — Claude Code now defaults to auto mode on Bedrock, Vertex AI, and Foundry, and updates Bedrock to Opus 4.8●GUARD — Auto mode now blocks tampering with session transcripts and asks before running rm -rf on an unresolved variable●IDP — Admins can provision MCP connectors org-wide via their IdP (starting with Okta), granting access on first login●TIMEOUT — Fixed per-server request_timeout_ms being ignored, which timed out long MCP tool calls at the 60s default●FAST — Fast mode for Opus 4.7 is deprecated and will be removed on July 24; migrate to fast mode for Opus 4.8
My MCP Timeout Was Being Ignored, and Every Call Died at Exactly 60 Seconds — Reclaiming Per-Server request_timeout_ms
A longer MCP tool call kept dying at exactly 60 seconds, ignoring the wait I had set. The cause: a per-server request_timeout_ms in .mcp.json that was never read, silently falling back to the default. Here is the correct placement after the fix, how to back the value out from a tool's response profile, and how to verify the cutoff yourself — with working code.
A nightly scheduled run was failing on one step with mechanical regularity. When I lined up the elapsed times from the log — 59.8s, 60.1s, 59.9s — the payload was different every time, but the moment of the cutoff was pinned to exactly 60 seconds. I was certain I had set a longer wait for that MCP server. No matter how many times I reread my config, the number was right. Yet what was actually in effect was not the value I wrote, but a default of 60 seconds living somewhere else.
The July 14 update explained the unease. A per-server request_timeout_ms written in --mcp-config or .mcp.json was not being read, and it quietly fell back to the global default. The fix landed. But what the fix gives you is only the premise that "the value you wrote will finally take effect" — not a decision about which value belongs where, or whether it is genuinely working. Today is my record of redesigning exactly that, anchored to how I run things at night as a personal developer.
An exact 60 seconds was not "the value I set" but "a default that got picked up"
Timeout incidents are treacherous because the cutoff itself looks like normal behavior. A heavy tool can exceed its wait, and being cut off then is arguably by design. So it is easy to shrug it off as "the tool just happened to be slow today."
But when the cutoff time clusters at nearly the same value every run, the story changes. Tool load varies day to day, so if only the cutoff stays constant, that is evidence of a fixed ceiling rather than load. In my case, that ceiling was not the value I had written — it was the default 60 seconds.
Some background. The wait time for an MCP tool call has several layers. One is the time to wait for the server to start; another is the time to wait for an individual tool to execute. The latter has a global default, which you can raise wholesale through the MCP_TOOL_TIMEOUT environment variable, and override per server with request_timeout_ms inside the server's definition in .mcp.json. The bug was that this per-server override was skipped, always falling back to the global default.
"Silently falling back to the default" is the hardest thing to catch
If a misconfiguration surfaced as an error, it would be easier. If startup told me "that value is invalid," I would notice on the spot. What made this case hard was that the ignored setting produced no warning at all — it simply ran on the default value as if nothing were wrong.
In unattended runs, that silence bites especially hard. Interactively you feel it — "huh, that cut off sooner than I expected" — but a scheduled log is read after the fact, and only the bare fact of the cutoff remains. Unless you deliberately record elapsed time, you cannot tell whether it was a configured cutoff or an unread default.
So the first move was not to fix the value, but to make the cutoff distribution visible. Line up the cutoff times for the same step across several runs and see whether they collapse to a single point. If they do, that is a sign the ceiling, not the load, is in charge. Whether or not you take this one small step changes how quickly you can self-diagnose whether a setting is working.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Recognize the bug where a per-server request_timeout_ms is ignored and falls back to the 60-second default, and confirm from your own log distribution whether the configured value is actually taking effect
✦Place per-server timeouts correctly in .mcp.json and understand their precedence over global defaults like MCP_TOOL_TIMEOUT, so long-running tool calls stay stable
✦Back timeout values out from each tool's response profile with a decision table, and drop in a lightweight checker that flags cutoffs pinned to the default — ready for unattended nightly runs
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Here is the correct placement after the fix. In .mcp.json, you write request_timeout_ms in milliseconds inside each server's definition. The points are that the unit is milliseconds, and that you deliberately give a value longer than the global default.
For a heavy tool like report-builder that aggregates AdMob daily revenue and generates a report as a personal developer, I give 180 seconds; for a light lookup like quick-lookup, I narrow it to 20. The key is not to make everything uniformly long. Giving a long ceiling to a light tool means that if it hangs, the wasted wait stretches out and eats the time left for later steps.
Keep the precedence in mind, too. The per-server request_timeout_ms wins first; a server without it falls to the global MCP_TOOL_TIMEOUT, and if that is unset, to the built-in default. The recent bug is, roughly, that this top-priority layer was not read and fell one step down.
If you only want to raise the floor globally, give it on the execution side:
# Set the tool-execution default to 120s. A per-server request_timeout_ms still wins.export MCP_TOOL_TIMEOUT=120000# Server startup wait is a separate track. Widen it for servers that start slowly.export MCP_TIMEOUT=30000
Tool-execution wait and server-startup wait are different things. Confusing when to touch MCP_TIMEOUT for a slow-starting server with when to extend request_timeout_ms for a heavy tool means the value you thought you fixed takes effect in the wrong layer. I got this backwards once, widened the startup timeout, and spent half a day wondering why the tool cutoff never changed.
Back the timeout value out from the tool's response profile
So how many seconds should it be? There is no single right answer here; backing it out from how the tool tends to respond proved most practical. Here is the rough guide I actually use, organized by the axis of judgment.
Tool character
Typical response
request_timeout_ms guide
Reasoning
Simple lookup / search
1–3s
15000–20000
If something that should be fast is slow, it is abnormal. Cut short and move to retry early
External API (one request)
3–10s
30000–45000
Leave room to absorb one or two rounds of the other side's latency
Aggregation / report build
20–90s
150000–180000
1.5–2x the worst case, but never above the whole run's budget
Large file processing
Variable, minutes
Cap at 1/3 of the run budget
Do not let one call eat the whole budget. Consider splitting first
More than the numbers themselves, two principles matter. One is to set the ceiling at 1.5–2x the measured "at most this long." Set it exactly, and you get an unstable gate that passes normally but fails only on high-load days. The other is that no matter how heavy a tool is, never allocate more than a third of the whole run's budget to a single call, because you stop being able to leave time for the remaining steps. Miss these two, and operations get rough even when you thought you set the values carefully.
After the fix, verify the cutoff yourself
Even once the update fixes the bug, it is worth separately confirming that your own setting takes effect as intended. Whether the value in the config file and the time actually cut off have drifted apart. I keep a light checker that pulls this from each run's log and watches for values pinned to the default.
import jsonimport statisticsfrom pathlib import Path# Read each server's request_timeout_ms from .mcp.json (convert to seconds)def configured_limits(config_path: str) -> dict[str, float]: cfg = json.loads(Path(config_path).read_text(encoding="utf-8")) limits = {} for name, server in cfg.get("mcpServers", {}).items(): ms = server.get("request_timeout_ms") if ms: limits[name] = ms / 1000.0 return limits# Decide whether cutoffs cluster at the default 60s rather than the configured valuedef detect_default_cap(timeouts_sec: list[float], configured_sec: float, default_sec: float = 60.0, tol: float = 1.5) -> str: if not timeouts_sec: return "no-data" median = statistics.median(timeouts_sec) near_default = abs(median - default_sec) <= tol near_configured = abs(median - configured_sec) <= tol if near_default and not near_configured and abs(configured_sec - default_sec) > tol: # Config is not 60s, yet the measurements pin to 60s = suspect fallback to default return "SUSPECT: cutoffs cluster at default, configured value not taking effect" return "ok"# Usage: pass the list of elapsed seconds cut off on the same steplimits = configured_limits(".mcp.json")print(detect_default_cap([59.8, 60.1, 59.9], configured_sec=limits.get("report-builder", 60.0)))
What it does is simple: it checks whether the median cutoff time sits near the configured value or leans toward the default 60 seconds. Because a run can hit 60 seconds by load alone, I never judge on a single value. I raise suspicion only when the median across several runs pins to the default and sits far from the configured value. Wire this one line into the tail of a scheduled run, and even if the same bug returns in a different form, it will not slip by in silence.
Fitting it into unattended nightly operation
When you delegate several jobs to run at night as a personal developer, a timeout becomes a tool for failing fast. In an unattended run, cutting short and moving on beats waiting forever, because the damage you see in the morning is smaller that way.
Through that lens, I set each per-server request_timeout_ms by backing it out from the whole run's budget. There is a total budget, inside it a per-step cutoff, and inside that the ceiling on a single tool call. Keeping this nesting — shorter as you go inward — is the foundation that prevents silent cutoff incidents. When a per-server value silently falls back to the default, as it did here, the innermost layer of that nesting drops out and gets swallowed by the larger outer ceiling. That is exactly why I want the small habit of confirming a value works, not just that it was written, permanently built into operations.
Configuration looks like a write-once affair, yet in practice it quietly betrays you like this. I am still feeling my way through much of it, but the habit of keeping the cutoff visible has become a quiet insurance against tasting the same surprise twice. Thank you for reading.
Share
Thank You for Reading
Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.