# Long-running tools have timeouts

- **Pattern:** `ab-001844` (`mcp-server.error-resilience.timeout-handling`)
- **Severity:** medium
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-001844
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-001844)

## Why it matters

A tool that calls `fetch()` with no timeout will hang indefinitely if the remote service is unresponsive — the MCP client freezes waiting for a result that never arrives. A tool that executes shell commands with no timeout can be kept running permanently by a hung subprocess, blocking the server for all clients. CWE-400 (uncontrolled resource consumption) is the mechanism; ISO 25010 performance-efficiency is the quality attribute violated. In agentic workflows where the AI chains multiple tool calls, a single hung tool halts the entire session.

## Severity rationale

Medium because missing timeouts cause hangs under degraded network or process conditions rather than immediate failures, but the impact is a frozen session with no recovery path short of restarting the server.

## Remediation

Add an `AbortController` timeout to every `fetch()` call and a timeout option to every shell command in tool handlers.

```ts
// src/tools/fetch.ts — 30-second timeout
server.tool('fetch_url', ..., async ({ url }) => {
  const controller = new AbortController()
  const timer = setTimeout(() => controller.abort(), 30_000)
  try {
    const res = await fetch(url, { signal: controller.signal })
    clearTimeout(timer)
    return { content: [{ type: 'text', text: await res.text() }] }
  } catch (error) {
    clearTimeout(timer)
    const msg = (error as Error).name === 'AbortError'
      ? `Request timed out after 30s: ${url}`
      : `Fetch failed: ${(error as Error).message}`
    return { content: [{ type: 'text', text: msg }], isError: true }
  }
})
```

## Detection

- **ID:** `timeout-handling`
- **Severity:** `medium`
- **What to look for:** Count all long-running operations in tool handlers. Enumerate which have timeout limits vs. which could run indefinitely. Identify tools that perform potentially long-running operations: HTTP requests, database queries, file system operations on large datasets, shell command execution, API calls to external services. Check whether these operations have timeouts configured. Check for `AbortController`/`AbortSignal` usage, `setTimeout` wrappers, or timeout options on HTTP clients (fetch timeout, axios timeout). Check that timeout errors are caught and returned as `isError: true` with a descriptive message.
- **Pass criteria:** Tools that make network requests, run queries, or execute commands have explicit timeouts. Timeout errors produce readable error messages (not raw timeout exceptions). At least 90% of tool handlers with external calls must have timeout limits of no more than 30 seconds.
- **Fail criteria:** Network-calling tools have no timeout (will hang indefinitely if the remote service is unresponsive), or timeout errors are not caught (crash the server).
- **Skip (N/A) when:** All tools are purely computational with no I/O operations. All checks skip when no MCP server is detected.

- **Cross-reference:** For graceful shutdown, see `graceful-shutdown`.
- **Detail on fail:** `"Tool 'fetch_url' uses fetch() with no timeout — will hang indefinitely if the target server is unresponsive. The MCP client will appear frozen"` or `"Tool 'run_command' executes shell commands with no timeout — a hanging process will block the server permanently"`
- **Remediation:** Add timeouts to all I/O operations:

  ```ts
  // src/tools/fetch.ts — timeout on external calls
  const controller = new AbortController()
  const timeout = setTimeout(() => controller.abort(), 30000)
  const response = await fetch(url, { signal: controller.signal })
  ```

  ```ts
  server.tool('fetch_url', ..., async ({ url }) => {
    try {
      const controller = new AbortController()
      const timeout = setTimeout(() => controller.abort(), 30_000)  // 30s timeout

      const response = await fetch(url, { signal: controller.signal })
      clearTimeout(timeout)

      const text = await response.text()
      return { content: [{ type: 'text', text }] }
    } catch (error) {
      if (error.name === 'AbortError') {
        return { content: [{ type: 'text', text: `Request timed out after 30 seconds: ${url}` }], isError: true }
      }
      return { content: [{ type: 'text', text: `Fetch failed: ${error.message}` }], isError: true }
    }
  })
  ```

## External references

- cwe CWE-400
- iso-25010 performance-efficiency

Taxons: error-resilience, cost-efficiency

HTML version: https://auditbuffet.com/patterns/ab-001844
