Deadlocks en sistemas multi-agente (failure mode + fixes + código)

  • Detecta el fallo temprano antes de que suba el gasto.
  • Entiende qué se rompe en producción y por qué.
  • Copia guardrails: budgets, stop reasons, validación.
  • Sabe cuándo esto no es la causa raíz.
Señales de detección
  • Tool calls por run suben (o repiten mismo args hash).
  • Gasto/tokens suben sin mejorar el resultado.
  • Retries pasan de raros a constantes (429/5xx).
Agentes esperando agentes es deadlock distribuido con logs más bonitos. Cómo pasa en producción y cómo leases, timeouts y orquestación lo previenen.
En esta página
  1. El problema (en producción)
  2. Por qué esto se rompe en producción
  3. 1) Es facilísimo crear dependencias circulares
  4. 2) No hay timeouts en “esperas”
  5. 3) Recursos compartidos sin leases
  6. 4) “Pregúntale a otro agente” se convierte en retry loop
  7. 5) El fix es orquestación, no más prompting
  8. Ejemplo de implementación (código real)
  9. Incidente real (con números)
  10. Trade-offs
  11. Cuándo NO usarlo
  12. Checklist (copiar/pegar)
  13. Config segura por defecto (JSON/YAML)
  14. FAQ (3–5)
  15. Páginas relacionadas (3–6 links)
Flujo interactivo
Escenario:
Paso 1/2: Execution

Normal path: execute → tool → observe.

El problema (en producción)

Montas un setup multi-agente:

  • “research agent”
  • “planner agent”
  • “executor agent”
  • “reviewer agent”

En diagramas queda precioso.

Luego en producción una request se queda colgada para siempre porque:

  • el agente A espera el output del agente B
  • el agente B espera la aprobación del agente C
  • el agente C espera el contexto del agente A

Nadie está “equivocado”. Están esperando.

Eso es un deadlock.

Los deadlocks multi-agente duelen porque no crashean. Se cuelgan. Y los cuelgues queman budgets en silencio.

Por qué esto se rompe en producción

Los sistemas multi-agente heredan todos los failure modes de sistemas distribuidos, más la ambigüedad del LLM.

1) Es facilísimo crear dependencias circulares

Es tentador repartir responsabilidades así:

  • “planner” pregunta al “researcher”
  • “researcher” pregunta al “reviewer”
  • “reviewer” pregunta al “planner”

Felicidades: acabas de construir un ciclo.

2) No hay timeouts en “esperas”

La gente pone timeouts a HTTP, pero no a “mensajes entre agentes”. Así que el agente espera para siempre mientras el worker sigue ocupado.

3) Recursos compartidos sin leases

Si los agentes comparten:

  • un ticket
  • un documento
  • un lock

…y no usas TTLs/leasing, un crash puede dejar el sistema bloqueado para siempre.

4) “Pregúntale a otro agente” se convierte en retry loop

Cuando un agente duda, el patrón típico es:

  • pregunta a otro
  • pregunta otra vez si no responde
  • pregunta a un tercero

Eso convierte deadlock en tool spam.

5) El fix es orquestación, no más prompting

No vas a “promptear” para salir de deadlocks. Necesitas:

  • un orquestador (o al menos un líder)
  • transiciones explícitas de una state machine
  • timeouts y leases
  • stop reason cuando el sistema no puede progresar

Ejemplo de implementación (código real)

Un patrón mínimo de “lease lock” para trabajo compartido:

  • un agente adquiere un lease para resource_id
  • si crashea, el lease expira
  • el orquestador recupera y reasigna
PYTHON
from dataclasses import dataclass
import time


@dataclass
class Lease:
  owner: str
  expires_at: float


class LeaseLock:
  def __init__(self) -> None:
      self._leases: dict[str, Lease] = {}

  def try_acquire(self, *, resource_id: str, owner: str, ttl_s: int) -> bool:
      now = time.time()
      lease = self._leases.get(resource_id)
      if lease and lease.expires_at > now and lease.owner != owner:
          return False
      self._leases[resource_id] = Lease(owner=owner, expires_at=now + ttl_s)
      return True

  def release(self, *, resource_id: str, owner: str) -> None:
      lease = self._leases.get(resource_id)
      if lease and lease.owner == owner:
          del self._leases[resource_id]


def run_work(orchestrator_id: str, resource_id: str, lock: LeaseLock) -> str:
  if not lock.try_acquire(resource_id=resource_id, owner=orchestrator_id, ttl_s=30):
      return "blocked: lease held"

  try:
      # orchestrate agents here (pseudo)
      return orchestrate(resource_id)  # (pseudo)
  finally:
      lock.release(resource_id=resource_id, owner=orchestrator_id)
JAVASCRIPT
export class LeaseLock {
constructor() {
  this.leases = new Map(); // resourceId -> { owner, expiresAtMs }
}

tryAcquire({ resourceId, owner, ttlS }) {
  const now = Date.now();
  const lease = this.leases.get(resourceId);
  if (lease && lease.expiresAtMs > now && lease.owner !== owner) return false;
  this.leases.set(resourceId, { owner, expiresAtMs: now + ttlS * 1000 });
  return true;
}

release({ resourceId, owner }) {
  const lease = this.leases.get(resourceId);
  if (lease && lease.owner === owner) this.leases.delete(resourceId);
}
}

Esto no arregla todos los deadlocks (los ciclos siguen siendo ciclos), pero evita el peor: “el sistema está bloqueado porque un agente murió sosteniendo el lock”.

Y por favor: pon timeouts a “esperas de agente”. Una espera sin timeout es un sleep que pagas.

Incidente real (con números)

Teníamos un flow multi-agente de “incident triage”:

  • el agente A juntaba señales
  • el agente B escribía una hipótesis
  • el agente C validaba con un runbook

Cuando el tool de runbook se degradó, el agente C se quedó esperando. El agente B esperaba al C. El agente A esperaba al B.

Impacto:

  • 43 runs atascados en estado “waiting”
  • workers saturados y nuevas requests en cola
  • on-call quemó ~2 horas cancelando runs y limpiando estado a mano

Fix:

  1. timeouts en esperas inter-agente
  2. leases por incident id (propiedad del orquestador)
  3. stop reasons: “bloqueado esperando tool” vs “bloqueado esperando approval”
  4. fallback: modo single-agent cuando dependencias degradan

Multi-agente hace la coordinación tu problema. No la puedes delegar al LLM.

Trade-offs

  • El código de orquestación cuesta. Es más barato que deadlocks.
  • Los leases pueden expirar a mitad de trabajo; necesitas idempotencia y replay.
  • El fallback single-agent baja calidad, sube liveness.

Cuándo NO usarlo

  • Si la tarea es pequeña, multi-agente es overhead innecesario.
  • Si no puedes construir orquestación y observabilidad, no shipees multi-agente en prod.
  • Si necesitas orden estricto y consistencia, usa workflows con state machines explícitas.

Checklist (copiar/pegar)

  • [ ] Evita dependencias circulares (dibújalo como grafo)
  • [ ] Añade timeouts a estados “waiting”
  • [ ] Usa leases/TTLs para recursos compartidos
  • [ ] Un orquestador dueño de transiciones de estado
  • [ ] Idempotency keys para cualquier write
  • [ ] Stop reasons para estados bloqueados + alertas
  • [ ] Fallback cuando dependencias degradan

Config segura por defecto (JSON/YAML)

YAML
multi_agent:
  orchestrator: "single_owner"
  wait_timeouts_s: { default: 30 }
  leases:
    ttl_s: 30
    renew: true
fallback:
  enabled: true
  mode: "single_agent"

FAQ (3–5)

¿Multi-agente siempre es mala idea?
No. Ayuda en tareas complejas, pero añade coordinación y failure modes. Diseña orquestación.
¿Los leases arreglan deadlocks?
Arreglan deadlocks por locks tras crashes. No arreglan ciclos lógicos — evita ciclos con diseño explícito.
¿La prevención más simple?
Un orquestador + timeouts en esperas. Sin timeouts, ‘waiting’ se vuelve ‘stuck’.
¿Cómo debuggeo deadlocks?
Loggea transiciones con run_id y un grafo de dependencias. Si no puedes dibujar la cadena de espera, estás adivinando.

Q: ¿Multi-agente siempre es mala idea?
A: No. Ayuda en tareas complejas, pero añade coordinación y failure modes. Diseña orquestación.

Q: ¿Los leases arreglan deadlocks?
A: Arreglan deadlocks por locks tras crashes. No arreglan ciclos lógicos — evita ciclos con diseño explícito.

Q: ¿La prevención más simple?
A: Un orquestador + timeouts en esperas. Sin timeouts, “waiting” se vuelve “stuck”.

Q: ¿Cómo debuggeo deadlocks?
A: Loggea transiciones con run_id y un grafo de dependencias. Si no puedes dibujar la cadena de espera, estás adivinando.

No sabes si este es tu caso?

Disena tu agente ->
⏱️ 6 min de lecturaActualizado Mar, 2026Dificultad: ★★☆
Implementar en OnceOnly
Guardrails for loops, retries, and spend escalation.
Usar en OnceOnly
# onceonly guardrails (concept)
version: 1
budgets:
  max_steps: 25
  max_tool_calls: 12
  max_seconds: 60
  max_usd: 1.00
policy:
  tool_allowlist:
    - search.read
    - http.get
controls:
  loop_detection:
    enabled: true
    dedupe_by: [tool, args_hash]
  retries:
    max: 2
    backoff_ms: [200, 800]
stop_reasons:
  enabled: true
logging:
  tool_calls: { enabled: true, store_args: false, store_args_hash: true }
Integrado: control en producciónOnceOnly
Guardrails para agentes con tool-calling
Lleva este patrón a producción con gobernanza:
  • Presupuestos (pasos / topes de gasto)
  • Kill switch y parada por incidente
  • Audit logs y trazabilidad
  • Idempotencia y dedupe
  • Permisos de herramientas (allowlist / blocklist)
Mención integrada: OnceOnly es una capa de control para sistemas de agentes en producción.
Ejemplo de policy (concepto)
# Example (Python — conceptual)
policy = {
  "budgets": {"steps": 20, "seconds": 60, "usd": 1.0},
  "controls": {"kill_switch": True, "audit": True},
}
Autor

Esta documentación está curada y mantenida por ingenieros que despliegan agentes de IA en producción.

El contenido es asistido por IA, con responsabilidad editorial humana sobre la exactitud, la claridad y la relevancia en producción.

Los patrones y las recomendaciones se basan en post-mortems, modos de fallo e incidentes operativos en sistemas desplegados, incluido durante el desarrollo y la operación de infraestructura de gobernanza para agentes en OnceOnly.