Guía9 min

Timeouts en tools HTTP: AbortSignal.timeout, no un sleep eterno

Resumen

Una tool de fetch sin signal cuelga el agente hasta el idle timeout del host. AbortSignal.timeout(ms) aborta con TimeoutError. Esta guía cubre fetch + signal, AbortSignal.any con cancelación del usuario, y por qué el timeout vive en la tool, no en un setTimeout olvidado. MDN + Node.

OpenAI
Una petición HTTP de una tool que se corta con AbortSignal al vencer el tiempo

Qué resuelve

Esta pieza se queda en la decisión práctica: qué instalar, qué riesgo agrega y cómo aplicarlo sin romper operación.

Un agente que llama fetch(url) sin signal hereda el peor timeout del runtime: a veces minutos, a veces nunca. Mientras tanto el loop sigue “pensando”, el usuario espera, y un retry duplica el side effect. El estándar ya tiene el corte: AbortSignal.timeout(ms).

Esto no es el retry/backoff del loop entero. Es el techo de una tool. Sandbox y secretos cubren otro borde; aquí es red.

Una línea

const res = await fetch(url, { signal: AbortSignal.timeout(8_000) });

MDN: AbortSignal.timeout(n) devuelve un signal que se aborta pasado n ms. El abort es un DOMException cuyo name es TimeoutError. No inventes new Promise((_, rej) => setTimeout(rej, n)) alrededor del fetch: no cancelas el socket.

Fetch con signal de timeout

Timeout + cancelación del usuario

Si el harness ya pasa un AbortSignal (el humano pulsó stop), no lo pises. Combina:

function toolSignal(user: AbortSignal | undefined, ms: number) {
  const t = AbortSignal.timeout(ms);
  return user && "any" in AbortSignal
    ? AbortSignal.any([user, t])
    : t;
}

AbortSignal.any aborta cuando cualquiera de la lista aborta. Node 20+ y browsers modernos. Si any no existe, usa el timeout solo y documenta el techo: el stop del usuario puede llegar tarde.

Catch:

try {
  return await fetch(url, { signal: toolSignal(ctx.signal, 8_000) });
} catch (e) {
  if (e instanceof DOMException && e.name === "TimeoutError") {
    throw new Error("tool_timeout: fetch > 8s");
  }
  throw e;
}

El mensaje tool_timeout es lo que el modelo ve. “Failed to fetch” no le enseña a acortar la URL.

Dónde vive el número

ToolTecho de partidaPor qué
HTTP GET docs8 sMDN/HTML cabe; si no, no es “un GET”
HTTP POST cobro15 sEl proveedor puede ser lento; igual hay techo
LLM interno (subllamada)el del SDKNo envuelvas otro modelo en 8 s a ciegas
Shellel del sandboxtimeout 30s cmd en Unix; no AbortSignal

Un default por tool en el registro (timeoutMs: 8000) gana a un global de 120 s que nunca muerde.

Retries de la tool: si reintentas un POST, idempotency key. Timeout ≠ “no pasó”; el servidor pudo ejecutar.

Registro de tools con timeoutMs

Checklist

  • Todo fetch en tools lleva signal.
  • AbortSignal.timeout, no Promise.race que deja el fetch vivo.
  • Error tipado TimeoutError → string estable al modelo.
  • Combina con el signal del usuario si existe.
  • POST + retry ⇒ clave de idempotencia (secretos no sustituyen esto).
  • Test: mock de fetch que nunca resuelve + expect timeout (Vitest, fake timers o un delay real corto).

El curso instalar un agente no pone techos de red. Sandboxing corta el FS, no un GET a un host lento.

FAQ

¿undici / Node 18 fetch? AbortSignal.timeout está en Node 17.3+ / 18. El de node-fetch v2 no. Sube o envuelve.

¿Axios timeout? Vale. Sigue siendo un techo de tool, no del agente.

¿3 s como Discord? Eso es el ack HTTP del bot, no el GET interno. Números distintos.

¿Timeout del modelo (max_tokens / SDK)? Otra capa. Esta guía es I/O de tools.

Un Promise.race que “gana” el timeout no aborta el fetch: el proceso sigue descargando y puede disparar el handler después. El signal es el corte real. Si ves CPU/red residual tras “timeout”, buscaste race, no AbortSignal.

Verificado 2026-09-03 contra MDN AbortSignal.timeout y Fetch API.