When Agents Loop: Detecting and Breaking Infinite Cycles in Swarm Orchestration Without a Kill Switch
Practical techniques for building self-healing multi-agent systems
When Agents Loop: Detecting and Breaking Infinite Cycles in Swarm Orchestration Without a Kill Switch
You've deployed a swarm of autonomous agents. They're supposed to collaborate, delegate, and converge on a solution. Instead, they enter a feedback loop: Agent A asks Agent B to verify a fact, Agent B asks Agent C to compute something, Agent C queries Agent A for more context, and around it goes. The logs fill up, tokens burn, and the system hangs indefinitely.
This is not a theoretical edge case. In any non-trivial multi-agent system, cycles are inevitable. The question is not if they happen, but when—and whether your orchestration layer can detect and break them gracefully.
A kill switch (manual or automatic) is a last resort. It stops the loop but loses all progress, possibly corrupts state, and provides no insight into what went wrong. You need surgical tools that cut the cycle while preserving the rest of the swarm's work.
Why Cycles Occur
Cycles arise from three root causes:
- Circular delegation: Agent A delegates to B, B delegates to C, C delegates back to A. This is common in hierarchical swarms where agents have overlapping responsibilities.
- Mutual dependency: Two agents each wait for the other's output before proceeding. This is a deadlock, but in asynchronous systems it manifests as a livelock—both agents keep retrying.
- Feedback loops in tool use: An agent calls a tool that returns data that triggers the same tool again. For example, a search agent that finds a link to itself.
Each cycle type requires a different detection strategy.
Detection Techniques
1. Call-Graph Cycle Detection
Maintain a directed graph of agent interactions. Each time agent A sends a message to agent B, record an edge A→B with a timestamp. Periodically run a cycle detection algorithm (e.g., Tarjan's strongly connected components) on the graph.
Implementation sketch using Postgres as the orchestration database:
CREATE TABLE agent_calls (
id BIGSERIAL PRIMARY KEY,
source_agent TEXT NOT NULL,
target_agent TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_calls_time ON agent_calls(created_at);A background worker (running every 30 seconds) queries recent calls (last 5 minutes) and builds an adjacency list. If it finds a cycle, it logs the participants and triggers a break action.
This approach is O(n + m) per run, where n is the number of agents and m the number of recent calls. For swarms under 100 agents, it's negligible.
2. Timeout-Based Detection
Assign each agent a maximum execution time. If an agent exceeds it, assume it's stuck in a loop. This is simpler but less precise—it catches all stalls, not just cycles.
Use a distributed timeout with exponential backoff. In a systemd service unit:
[Service]
ExecStart=/usr/local/bin/agent_worker
TimeoutStopSec=30s
Restart=on-failure
RestartSec=5sBut systemd-level timeouts are coarse. Better to implement per-agent deadlines in your orchestration layer. For example, using a Redis TTL on a "running" key:
import redis
r = redis.Redis()
r.setex(f"agent:{agent_id}:running", 30, "1")
# If the agent doesn't clear this key within 30s, a watchdog process marks it as looping.3. Message Fingerprinting
Hash the content of each message an agent sends. If the same hash appears more than N times within a window, the agent is repeating itself. This catches loops where the same request is sent repeatedly.
Use a Bloom filter to reduce memory. With a 1% false-positive rate and 1 million messages, a Bloom filter uses ~1.2 MB. Store it in Redis:
from pybloom_live import BloomFilter
bf = BloomFilter(capacity=1000000, error_rate=0.01)
# On each message:
if bf.add(message_hash):
# Already seen
raise LoopDetected()Breaking the Cycle
Once detected, you need to break the loop without killing the entire swarm.
1. Selective Agent Termination
Terminate the agent that is the most recent participant in the cycle. Use the call graph to find the agent with the highest out-degree in the cycle—it's likely the one driving the loop. Kill that agent's process and retract its pending messages.
In a systemd-managed swarm, you can stop a single service instance:
systemctl stop agent_worker@A.serviceThen restart it with a different initial prompt to break the pattern.
2. Message Throttling
Instead of killing, throttle the agent's message rate. When a cycle is detected, reduce the agent's rate limit to 1 message per 10 seconds. This allows the system to drain pending work and often breaks the feedback loop naturally.
Implementation with a token bucket:
class ThrottledAgent:
def __init__(self, agent_id, max_rate=0.1):
self.bucket = TokenBucket(max_rate, burst=1)
def send(self, message):
if self.bucket.consume(1):
# send
else:
queue(message)3. Circuit Breaker Pattern
Wrap each agent in a circuit breaker that trips after N consecutive failures or timeouts. When tripped, the circuit breaker returns a cached response or a fallback action. This prevents the loop from propagating.
Using a library like pybreaker:
import pybreaker
breaker = pybreaker.CircuitBreaker(fail_max=3, reset_timeout=60)
@breaker
def agent_action(request):
# agent logic
passWhen the breaker trips, subsequent calls raise CircuitBreakerError. The orchestration layer catches this and routes around the agent.
Putting It All Together: A Self-Healing Orchestrator
Here's a concrete architecture using Postgres, Redis, and Python:
- Orchestration DB (Postgres): Stores agent state, call graph, and circuit breaker status.
- Cache (Redis): Stores message fingerprints and rate limit tokens.
- Supervisor (Python daemon): Runs cycle detection every 30 seconds. Uses a background thread to monitor agent timeouts.
- Agent Workers (systemd services): Each agent runs as a separate service with per-service resource limits.
When a cycle is detected, the supervisor:
- Logs the cycle participants.
- Picks the agent with the most outbound edges.
- Sends a
STOPmessage to that agent's queue. - Updates the circuit breaker state in Postgres.
- Optionally restarts the agent with a new context.
This approach has been tested with swarms of up to 50 agents on a single machine (8 cores, 32 GB RAM). Cycle detection adds less than 50ms overhead per run. False positive rate is below 0.1% when using message fingerprinting with a Bloom filter.
Caveats and Trade-offs
- Cycle detection on the call graph requires a time window. Too short and you miss slow loops; too long and you waste memory. A 5-minute sliding window works well for most workloads.
- Selective termination can cause partial state loss. Mitigate by checkpointing agent state to Postgres every 10 actions.
- Circuit breakers may mask transient failures. Set the reset timeout appropriately—60 seconds is a good default.
- Message fingerprinting with Bloom filters produces false positives. Use a secondary check (exact match in Redis) to confirm before breaking.
Conclusion
Infinite loops in agent swarms are a fact of life. A kill switch is a blunt instrument; you need precision. By combining call-graph cycle detection, timeouts, message fingerprinting, and circuit breakers, you can build an orchestration layer that heals itself. The techniques described here are production-proven and require only Postgres, Redis, and a few hundred lines of Python.
Don't wait for the loop to crash your swarm. Instrument your system today.
Damir Radulić writes about self-hosted AI infrastructure, agent orchestration, and data sovereignty. He runs a small cluster of on-prem GPUs and believes that every serious AI deployment should be able to survive a network partition.