DEV Community

Finley Zhu
Finley Zhu

Posted on

Designing for Shared LLM Infrastructure: Timeouts, Retries, and Circuit Breakers That Actually Work

Shared LLM endpoints fail differently than dedicated ones, and your client code is the only thing you control. A free server such as MonkeyCode's option is shared by design, which means latency spikes and transient errors are normal. The correct response is not to avoid free tiers but to build a client that assumes unreliability. This article shows a concrete pattern for timeouts, retries, and circuit breakers.

Disclosure: This article was prepared as part of MonkeyCode's product outreach.

Why Shared Endpoints Behave Differently

A shared endpoint serves many tenants at once, so the latency of any single request depends on everyone else's load. This creates a heavy-tailed distribution where occasional slow requests are an order of magnitude slower than the median. If you optimize only for average latency, your pipeline will collapse exactly when it needs stability. Understanding this is the starting point for designing a resilient client.

  • Multi-tenant noise: your request competes with unrelated workloads.
  • Heavy-tailed latency: p99 can be ten times higher than p50.
  • Transient errors: connection resets and 429s are the norm, not exceptions.

The Three Pillars of a Resilient Client

  • Timeouts: every request needs an explicit deadline, or a hung request will occupy your worker forever.
  • Retries with backoff: retries must use exponential backoff plus jitter to avoid a thundering herd.
  • Circuit breaker: when consecutive failures exceed a threshold, fail fast instead of hammering an unhealthy server.

A Minimal Python Implementation

The following class wraps any OpenAI-compatible endpoint with the three pillars. It uses httpx for async HTTP and a simple state machine for the circuit breaker.

import asyncio
import random
import httpx

class ResilientClient:
    def __init__(self, base_url, api_key, model, timeout=30.0, max_retries=3, circuit_threshold=5):
        self.base_url = base_url
        self.api_key = api_key
        self.model = model
        self.timeout = timeout
        self.max_retries = max_retries
        self.circuit_threshold = circuit_threshold
        self.failures = 0
        self.circuit_open = False

    async def complete(self, prompt: str) -> str:
        if self.circuit_open:
            raise RuntimeError("Circuit is open; failing fast")
        for attempt in range(self.max_retries):
            try:
                response = await self._call(prompt)
                self.failures = 0
                return response
            except (httpx.TimeoutException, httpx.HTTPStatusError) as exc:
                self.failures += 1
                if self.failures >= self.circuit_threshold:
                    self.circuit_open = True
                if attempt == self.max_retries - 1:
                    raise
                await asyncio.sleep(min(2 ** attempt + random.random(), 8))
        raise RuntimeError("Unreachable")

    async def _call(self, prompt: str) -> str:
        async with httpx.AsyncClient(timeout=self.timeout) as client:
            resp = await client.post(
                f"{self.base_url}/v1/chat/completions",
                headers={"Authorization": f"Bearer {self.api_key}"},
                json={"model": self.model, "messages": [{"role": "user", "content": prompt}]},
            )
            resp.raise_for_status()
            return resp.json()["choices"][0]["message"]["content"]
Enter fullscreen mode Exit fullscreen mode

The key detail is the backoff calculation: min(2 ** attempt + random.random(), 8) keeps the wait time bounded while adding jitter. The circuit breaker opens after five consecutive failures, which prevents your batch job from wasting time on a dead endpoint. When the circuit is open, the client raises immediately, allowing your orchestration layer to handle the failure gracefully.

Tuning the Parameters

  • Timeout: start with 30 seconds and measure your p95 latency against the shared server.
  • Max retries: three is a good default; more than five usually means the server is down.
  • Circuit threshold: five failures works for batch jobs; lower it for interactive workloads.

A Simple Test Harness

You can validate the client against a mock server before pointing it at a real endpoint. This FastAPI app simulates a slow shared server:

import asyncio
from fastapi import FastAPI
import uvicorn

app = FastAPI()

@app.post("/v1/chat/completions")
async def mock_completion():
    await asyncio.sleep(1)  # simulate latency
    return {"choices": [{"message": {"content": "ok"}}]}

# Run with: uvicorn mock_server:app --port 8000
Enter fullscreen mode Exit fullscreen mode

Then run the client with a short timeout to confirm retries work:

async def main():
    client = ResilientClient("http://localhost:8000", "test", "mock", timeout=0.5)
    try:
        print(await client.complete("hello"))
    except RuntimeError as e:
        print(e)

asyncio.run(main())
Enter fullscreen mode Exit fullscreen mode

This harness lets you observe backoff behavior and circuit opening without spending tokens. It also gives you a reproducible way to compare different timeout settings.

When This Pattern Pays Off

Scenario Use shared free server? Client pattern
Offline batch scoring Yes Retries + circuit breaker
Interactive chat No Dedicated endpoint
CI smoke tests Yes Short timeout, no retries
Regulated data Never Self-hosted

Limitations and Who Should Not Use This

Free servers do not provide an SLA, so this pattern cannot make them suitable for production-critical paths. If your prompts contain regulated or confidential data, a shared endpoint is the wrong choice regardless of client design. If you need consistent low latency for user-facing features, no amount of retry logic will fix an overloaded server. This approach is for workloads that can tolerate occasional delays and need to survive intermittent failures.

The Position, Restated

Free tiers are not a trap; they are a testing ground for exactly the resilience your production code needs. By building timeouts, retries, and circuit breakers into your client, you turn an unreliable endpoint into a usable one. The next time your batch job struggles with a shared server, resist the urge to blame the provider. Harden your client first, and MonkeyCode's free server is a perfectly honest place to practice that.

MonkeyCode provides free models that can run this workflow.

Top comments (0)