← Back to insights
Field note6 min readAgentify

Scaling AI Agents from 10 to 10,000 Users — Architecture Patterns That Work

Your agent works perfectly with five beta testers. You open it to 500 users and everything falls apart — responses take 30 seconds, your OpenAI bill goes through the roof, and the agent star

Your agent works perfectly with five beta testers. You open it to 500 users and everything falls apart — responses take 30 seconds, your OpenAI bill goes through the roof, and the agent starts hallucinating because the retrieval pipeline can't keep up.

I've been there. Here's what I've learned.


Scaling AI agents is not like scaling a REST API. A typical API endpoint is stateless, deterministic, and finishes in milliseconds. An AI agent might be stateful, is definitely non-deterministic, takes 5–30 seconds per invocation, and each call costs real money in LLM API fees.

The architecture patterns that work for CRUD apps don't translate. You need different patterns — and the right pattern depends on which scaling dimension is killing you.


First: know which dimension you're scaling

Before picking an architecture, figure out what's actually breaking:

Concurrency — how many users can hit the system at once? Constrained by LLM rate limits, compute resources, and session management.

Latency — how long does each request take? Agents with multiple tool calls compound latency at every step. A 3-step agent with 2-second LLM calls takes 6+ seconds minimum.

Cost — at 10 users, your OpenAI bill is ₹5,000/month. At 10,000, it could be ₹50 lakh if you're not careful.

Reliability — what happens when something fails mid-conversation? Does the user lose their entire context?

Cleanlab's 2025 production survey found that most teams with agents in production rebuild their stack every three months. The ground shifts fast. So whatever you build, make it modular.


Pattern 1: Stateless agent pools

The simplest scaling pattern. Deploy multiple identical agent instances behind a load balancer. Each handles requests independently.

MLMastery's deployment guide describes it well: multiple instances sit behind a load balancer, auto-scaling based on queue depth or response latency. If one fails, others keep working.

# Kubernetes HPA config
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
spec:
  scaleTargetRef:
    kind: Deployment
    name: agent-service
  minReplicas: 2
  maxReplicas: 20
  metrics:
    - type: Pods
      pods:
        metric:
          name: requests_in_queue
        target:
          type: AverageValue
          averageValue: "5"

Use when: Single-turn tasks — document analysis, classification, data extraction, one-shot Q&A. Anything where each request is independent.

The catch: No memory between turns. For multi-turn conversations, you're sending the entire history with every request, which gets expensive fast.


Pattern 2: Externalised session state

For conversational agents — support bots, coding assistants, research agents — you need state management. But don't use sticky sessions (routing a user to the same instance). That breaks auto-scaling: if an instance dies, all its sessions die with it.

Instead, externalise state to a shared store:

class AgentSessionManager:
    def __init__(self):
        self.redis = redis.Redis(host='redis-cluster', port=6379)
        self.ttl = 3600  # 1 hour timeout

    def get_session(self, session_id):
        data = self.redis.get(f"session:{session_id}")
        return json.loads(data) if data else {"history": [], "cache": {}}

    def save_session(self, session_id, state):
        self.redis.setex(f"session:{session_id}", self.ttl, json.dumps(state))

Now any instance can handle any request — just pull the session from Redis, process, save it back. MLMastery recommends Redis for short conversations (minutes to hours) and PostgreSQL/DynamoDB for longer ones (days to weeks).

Pro tip: Don't store the full conversation history forever. Implement a sliding window or rolling summarisation — compress older turns into a summary to keep token costs manageable.


Pattern 3: Queue-based async processing

Not everything needs a synchronous response. For document analysis, batch tasks, or complex multi-step workflows, an async queue is way more scalable.

User → API Gateway → Message Queue → Agent Workers → Results Store → Webhook

The API accepts the request, returns a job ID immediately, and workers process from the queue when capacity is available. This gives you natural backpressure (requests queue up instead of timing out), retry logic (failed tasks go back in the queue), and priority lanes (premium users get a fast queue).

@celery_app.task(bind=True, max_retries=3)
def process_agent_task(self, payload):
    try:
        result = run_agent(payload['query'], payload['context'])
        save_result(payload['job_id'], result)
        notify_user(payload['callback_url'], result)
    except RateLimitError:
        self.retry(countdown=60)

This is my go-to for anything that doesn't need a real-time response. It's simpler to operate, cheaper to run, and way more resilient than synchronous processing at scale.


Pattern 4: Hierarchical multi-agent systems

For complex workflows, one agent trying to do everything becomes both a bottleneck and a quality risk. Use a supervisor that delegates to specialised workers.

There's excellent research backing this up. Kim et al.'s 2025 paper from Google Research evaluated five agent architectures across multiple benchmarks. Their key finding: centralised systems with an orchestrator achieved the best balance between success rate and error containment. Independent agents (working in parallel without coordination) amplified errors by up to 17.2x. The orchestrator acts as a validation layer, catching mistakes before they cascade.

They also found that multi-agent coordination boosted performance by 81% on parallelisable tasks (financial reasoning), but hurt performance on sequential ones. The lesson: decompose your workflow and match the coordination pattern to the task structure.

class SupervisorAgent:
    async def process(self, task):
        plan = await self.create_plan(task)
        results = {}
        for step in plan.steps:
            if step.can_parallel:
                step_results = await asyncio.gather(*[
                    self.workers[s.worker].execute(s) for s in step.sub_steps
                ])
            else:
                step_results = await self.workers[step.worker].execute(step)

            # Supervisor validates each step before continuing
            if not await self.validate(step, step_results):
                step_results = await self.retry_or_escalate(step)
            results[step.id] = step_results
        return self.synthesise(results)

Scaling advantage: Each worker type scales independently. If retrieval is the bottleneck, scale up retrieval workers. Use expensive models for the supervisor and cheap ones for simple workers.


Pattern 5: Cost-aware scaling (because money is real)

At 10,000 users, LLM costs dominate your bill. These optimisations matter:

Semantic caching. Don't call the LLM twice for the same question. If someone asks "what's our refund policy?" and someone else asks "how do I get a refund?", a cached response from the first can serve the second. GPTCache or custom implementations with embedding similarity can cut LLM calls by 30–60%.

Model routing. Not every query needs GPT-4o. Build a simple classifier at the entry point: greetings and FAQs go to GPT-4o-mini (cheap, fast), complex reasoning goes to GPT-4o (expensive, smart). This alone can cut costs 40–60%.

Prompt compression. As conversations grow, token costs grow linearly. Summarise older turns instead of sending the full history.

Batch API. For async workloads, OpenAI's Batch API gives 50% cost reduction for non-time-sensitive tasks.

Streaming. Stream responses token-by-token. Doesn't reduce cost, but users see the first token in 200ms instead of waiting 5 seconds. Perceived latency drops dramatically.


What to monitor at scale

Google Cloud's guide on scalable agents stresses that without comprehensive monitoring, diagnosing issues becomes impossible at scale.

Here's what I track:

Infrastructure: Queue depth, worker utilisation, pod count, auto-scaler events.

Agent: Requests per second, p50/p95/p99 latency, error rate, guardrail trigger rate.

LLM: Tokens per request, cost per request, rate limit hits, model error rates.

Quality: Task completion rate, hallucination rate, user satisfaction (thumbs up/down), escalation rate.

Business: Cost per resolution, deflection rate, accuracy on test sets.


Quick reference: what to build at each stage

10 users (pilot): Single instance, sync processing, in-memory state. Focus on quality, not infra. Log everything.

100 users (team): Load balancer + Redis for sessions. Basic monitoring. Semantic caching. Guardrails.

1,000 users (department): Kubernetes with auto-scaling. Queue-based processing for long tasks. Model routing. Cost dashboards. PII detection.

10,000 users (company-wide): Multi-worker agents. Priority queues. Advanced caching. Batch API. Multi-region. Comprehensive audit trails. Monthly cost reviews.


The pattern I see over and over: teams build for the demo, hit a wall at 100 users, then scramble to re-architect. Don't do that. Start with the simplest pattern that works, but design it to evolve. Externalise state from day one. Containerise from day one. Log from day one.

Then when the scaling challenge hits — and it will — you're extending an architecture, not rebuilding one.