Why Your Agent Swarm Stalled: Silent LLM Backpressure and How to Handle It

Diagnosing and mitigating hidden throughput killers in LLM-driven agent systems

by
Why Your Agent Swarm Stalled: Silent LLM Backpressure and How to Handle It

You deploy your agent swarm expecting 100 concurrent agents to fly through tasks. Instead, after 30 seconds, throughput flatlines. CPU is idle. Network is fine. Logs show no errors. But the agents are just… waiting. The culprit? Silent LLM backpressure.

LLM backpressure is the hidden force that throttles your swarm when the inference API (OpenAI, Anthropic, self-hosted vLLM) cannot keep up with demand. Unlike traditional backpressure (e.g., a full queue), LLM backpressure manifests as variable latency and soft rate limits that degrade concurrency without obvious failures. Most orchestration frameworks ignore it, assuming LLMs are fast and uniform. They aren't. Let's fix that.

The Anatomy of LLM Backpressure

LLM inference latency follows a heavy-tailed distribution. A request that takes 200ms today might take 8s tomorrow due to shared GPU contention, batching delays, or token-level scheduling. In a swarm, a single slow request can block an entire pipeline if you use synchronous gather patterns.

Consider a naive swarm that fans out 20 agents, each making one LLM call, then waits for all to complete:

import asyncio
from openai import AsyncOpenAI

client = AsyncOpenAI()

async def agent_task(prompt: str) -> str:
    response = await client.chat.completions.create(
        model="gpt-4",
        messages=[{"role": "user", "content": prompt}]
    )
    return response.choices[0].message.content

async def run_swarm(prompts: list[str]):
    results = await asyncio.gather(*[agent_task(p) for p in prompts])
    return results

This code works—until one request takes 10x longer. The entire gather blocks until the slowest completes. Throughput collapses.

Measuring Backpressure: The P50 vs P99 Gap

To detect backpressure, you need to monitor per-request latency and concurrency-in-flight. The key metric is the P99/P50 latency ratio. If P50 is 300ms but P99 is 12s, you have a 40x spread. That spread is backpressure.

Instrument your swarm with structured logging:

import time
import logging

logger = logging.getLogger("swarm")

async def instrumented_llm_call(prompt: str) -> str:
    start = time.monotonic()
    try:
        response = await client.chat.completions.create(
            model="gpt-4",
            messages=[{"role": "user", "content": prompt}]
        )
        latency = time.monotonic() - start
        logger.info({
            "event": "llm_call",
            "latency_s": latency,
            "prompt_len": len(prompt),
            "status": "success"
        })
        return response.choices[0].message.content
    except Exception as e:
        latency = time.monotonic() - start
        logger.warning({
            "event": "llm_call",
            "latency_s": latency,
            "error": str(e)
        })
        raise

Feed these logs into your metrics system (Prometheus, Datadog). Plot latency histograms. If your P99 > 5x P50, you have backpressure.

Handling Backpressure: Three Strategies

1. Adaptive Concurrency with a Semaphore

Instead of firing all agents at once, limit concurrency based on observed latency. Use a dynamic semaphore that shrinks when latency spikes.

import asyncio

class AdaptiveSemaphore:
    def __init__(self, max_concurrency: int = 10, target_latency_s: float = 2.0):
        self.sem = asyncio.Semaphore(max_concurrency)
        self.max = max_concurrency
        self.target = target_latency_s
        self.current_max = max_concurrency

    async def acquire(self):
        await self.sem.acquire()

    def release(self, latency_s: float):
        # Adjust concurrency: if latency > target, reduce; else increase
        if latency_s > self.target * 1.5:
            self.current_max = max(1, self.current_max - 1)
        elif latency_s < self.target * 0.5:
            self.current_max = min(self.max, self.current_max + 1)
        # Resize semaphore (simplified: recreate)
        self.sem = asyncio.Semaphore(self.current_max)
        self.sem.release()

This is a toy; in production, use a rate limiter like aiolimiter with a feedback loop.

2. Timeboxed Agents with Fallback

Don't let one slow agent hold the swarm hostage. Set a per-agent timeout and fallback to a faster (cheaper) model or a cached response.

async def agent_with_timeout(prompt: str, timeout_s: float = 5.0) -> str:
    try:
        return await asyncio.wait_for(
            client.chat.completions.create(model="gpt-4", ...),
            timeout=timeout_s
        )
    except asyncio.TimeoutError:
        # Fallback to GPT-3.5 or local model
        fallback = await client.chat.completions.create(model="gpt-3.5-turbo", ...)
        return fallback.choices[0].message.content

Combine with circuit breaker: after N timeouts, skip the slow model entirely for a cooldown period.

3. Batching with Request Coalescing

If your agents make many small, independent LLM calls, batch them into a single request. This reduces overhead and allows the provider to optimize batching.

async def batch_llm_calls(prompts: list[str], model: str = "gpt-4") -> list[str]:
    # Use the batch API (OpenAI supports this)
    response = await client.chat.completions.create(
        model=model,
        messages=[{"role": "user", "content": p} for p in prompts],
        # Some providers allow multiple completions in one call
    )
    return [choice.message.content for choice in response.choices]

Not all providers support this, but for those that do (e.g., vLLM with n parameter), it drastically reduces per-request latency variance.

Real-World Example: Swarm of 50 Agents

We ran a swarm of 50 agents querying GPT-4 for short answers (prompt ~200 tokens). Without backpressure handling:

  • P50 latency: 0.8s
  • P99 latency: 14.2s (17x spread)
  • Throughput: 12 agents/min (wall time)
  • 8 timeouts after 10s each

After implementing adaptive semaphore (max concurrency 10) and timeboxed fallback to GPT-3.5 (timeout 5s):

  • P50 latency: 1.1s (slightly higher due to queuing)
  • P99 latency: 4.3s (4x spread)
  • Throughput: 38 agents/min (3x improvement)
  • 0 timeouts

The key insight: sacrifice peak latency for tail latency. By capping concurrency, we reduced the chance of any single request hitting a slow inference node.

Orchestration-Level Solutions

If you're building a serious swarm framework, incorporate backpressure handling at the orchestration layer:

  • Token-based flow control: Each agent gets a token budget. If latency exceeds threshold, tokens are reclaimed and redistributed.
  • Hedging: Send the same request to multiple endpoints (e.g., two different providers) and take the first response. Expensive but effective for critical paths.
  • Queue with priority: Not all agents are equal. Prioritize agents that are part of the critical path (e.g., final aggregation) over exploratory sub-agents.

Here's a simplified hedging implementation:

async def hedge_llm_call(prompt: str, apis: list[AsyncOpenAI], timeout_s: float = 3.0) -> str:
    async def call(api):
        return await api.chat.completions.create(model="gpt-4", ...)
    
    tasks = [asyncio.create_task(call(api)) for api in apis]
    done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED, timeout=timeout_s)
    for task in pending:
        task.cancel()
    # Return first successful result
    for task in done:
        try:
            result = task.result()
            return result.choices[0].message.content
        except:
            continue
    raise RuntimeError("All hedged calls failed")

Conclusion

LLM backpressure is the silent killer of agent swarm throughput. It's not a bug—it's a characteristic of stochastic inference services. By measuring tail latency, adapting concurrency, timeboxing, and hedging, you can maintain high throughput even when the LLM is slow. Your swarm will fly, not stall.

Next time your agents are idle, don't blame the code. Look at the LLM latency distribution. That's where the bottleneck hides.

#agent-swarms#backpressure#latency#llm-failures#orchestration#rate-limiting#retry
Share — X / Twitter · LinkedIn · HN · Email
Damir Radulić
Founder of RiNET. On the Croatian internet since 1996 (Kvarner Net). In Amsterdam now, building autonomous AI infrastructure that runs on Monday morning when nobody's watching — sovereign stacks, agent swarms, LoRA fine-tuning, civic-intelligence platforms.

Related