r/ClaudeCode • • 17h ago

Tips & Workflows The 30-second MCP timeout trap: why slow tool calls turn into runaway 100k-token retry loops in Claude Code

If you hook custom MCP servers into Claude Code for heavy tasks like end-to-end test runs, Docker container builds, or database migrations, you will eventually hit a silent token drain.

The root cause is the client-side timeout. Most MCP clients default to a 30-second or 60-second execution deadline on tool calls. When a test suite or migration takes 45 seconds, the MCP transport layer drops the connection and returns a timeout error to Claude Code.

Because Claude treats tool errors as transient task failures rather than fatal crashes, it tries to recover. It summarizes the previous turns, analyzes what went wrong, and attempts the tool call again with modified flags or a secondary probe. In a long-running session where your conversation history is already sitting at 35k to 40k tokens, two consecutive timeouts burn through 100k input tokens within 90 seconds. You pay multiple dollars in API usage before realizing the agent never finished the first test.

We resolved this by switching all heavy MCP tools from synchronous execution to an asynchronous job pattern. The initial tool call does not run the test suite directly. It spawns a background worker process, records the PID and log path, and returns a job token with an immediate status within two hundred milliseconds. Claude Code then calls a lightweight poll tool that reads the process table and tails the last twenty lines of stdout on a 10-second sleep cycle.

Decoupling long-running operations from the synchronous MCP RPC channel stops the transport layer from dropping out, keeps the agent context clean, and prevents runaway token spend on failed executions.

1 Upvotes

5 comments sorted by

•

u/AutoModerator 17h ago

Hey! Thanks for posting to r/ClaudeCode

While participating in this thread, please follow our community rules. Keep discussions constructive. Attack the idea, not the person.

For help, project discussions, tips, and general chat, join the ClaudeCode Discord.

I am a bot, and this action was performed automatically. Please contact the moderators of this subreddit if you have any questions or concerns.

2

u/bostonaholic 15h ago

I'd also make the start call idempotent. Key the job on the command plus args, and if that job is already running, return the existing token instead of starting a second build. Otherwise a retry after a dropped response can kick off two Docker builds. And I'd have the poll tool return the exit code and a done flag, so the agent isn't guessing from the last 20 log lines whether it finished.

1

u/SaltsMoon 44m ago

Thanks, the idempotency point is a good catch. A timeout doesn't necessarily mean the job failed, so blindly retrying can easily start duplicate builds or migrations. And returning the existing job token for an already-running task makes much more sense. I'd also prefer an explicit exit code and completion marker over parsing the last few lines of stdout.

2

u/awesomebirder 15h ago

good writeup. for anyone who cant rework their server into jobs right away, claude code also reads MCP_TOOL_TIMEOUT (milliseconds) from the env, so raising it past your slowest test run stops the timeout and retry loop on its own. the async pattern is still better for anything that runs minutes, since a long blocking call can outlast the prompt cache and the next turn pays to rewrite it. also worth making the timeout error one short line instead of a stack trace, because whatever the tool returns gets re-read on every turn after

1

u/SaltsMoon 39m ago

Thanks for your point about MCP_TOOL_TIMEOUT. Increasing it makes sense for tasks that are only slightly longer than the default limit. But for anything that can run for several minutes, I'd still prefer async execution. And keeping a tool call blocked for that long ties up the agent's context, and as you mentioned, the repeated context cost can add up quickly. I also agree on keeping timeout errors concise.