๐Ÿงฉ Agentic Scaling how consumer AI scales ยท 1 โ†’ 1B

The Fleet โ€” stateless workers and the control plane

Every person chatting with an assistant is served by some worker, somewhere. Whether a product survives its own growth turns not on "how fast is one worker" but on "what must a worker know, and who decides how many exist." This chapter answers both: the stateless worker contract, and the control plane that reconciles the fleet to a desired state.

The thesis, said this chapter's way: scale is not a bigger box. It is many identical workers that hold nothing, a shared set of stores that hold everything, and one small loop โ€” the same loop inside Kubernetes' HPA and inside every managed autoscaler โ€” that keeps the count of workers matched to demand in both directions.

๐Ÿ“– Story: the 3 a.m. capacity meeting

Before desired-state loops, capacity was a person. Traffic spiked, a pager went off, someone added machines by hand โ€” then forgot to remove them, so the bill stayed high for a month. Each machine had been lovingly configured, so replacing one meant remembering what made it special. The industry's answer was two changes at once: make the machines boring (any one replaces any other), and replace the person with a loop that compares what is running to what should be and closes the gap. Assistants inherit that answer wholesale; the only new ingredient is that "what should be running" is measured in people served, and even that reduces to the same arithmetic.

What does "stateless" actually promise?

A stateless worker promises that nothing about a person lives inside it between turns. Concretely, the contract has four clauses:

  1. Pull, never hold. At the start of a turn the worker resolves the user id, pulls the session, the profile via keyed lookup, and top-K episodic memory from the user's memory namespace, and assembles the context pack. When the turn ends, it forgets all of it.
  2. Write to shared stores only. Anything learned goes out through write-back to the memory and session stores โ€” never into a local variable, disk, or cache another worker cannot see.
  3. Any worker, any user. Because of 1 and 2, the next turn for the same person may land on a different worker with no loss. Routing can be by least load, region, or model tier โ€” whatever the control plane finds convenient.
  4. Replaceable without ceremony. A worker can start, stop, or crash mid-turn and the only cost is one retried turn. Nothing is drained, migrated, or backed up.
flowchart LR
    U[Person on a phone] --> R[Router]
    R --> W1[Worker]
    R --> W2[Worker]
    W1 --> S[(Session store)]
    W1 --> P[(Profiles + memory)]
    W2 --> S
    W2 --> P
    W1 --> M[Model service]
    W2 --> M

Everything the memory chapter built โ€” the namespace, the keyed profile, the async write-back โ€” exists to make clause 1 cheap and clause 2 safe. The worker is stateless because the stores are well designed.

๐Ÿชค Misconception โ€” "stateless means the assistant can't remember me." The opposite: statelessness is what lets memory survive a worker being replaced. State that lives in a worker dies with it.

What is the control plane deciding?

One number per tier: how many workers should exist right now. It comes from two inputs โ€” an observation (how many people are active) and a policy (how many people one worker should serve, the users per instance ratio) โ€” and one line of arithmetic:

TEXT
desired = ceil(active_users / users_per_instance)

Everything else the control plane does is bookkeeping around that line: which worker each active person is assigned to, which workers are idle, and a record of every scale event so observability can show why the fleet changed. The desired state is not a command ("start three workers") but a target ("there should be three"). That distinction is what makes the loop below safe to run forever.

The reconcile loop

The loop runs on every event that could move either input โ€” a person becoming active or going quiet, an operator editing the policy โ€” and on a timer as a safety net. It is short enough to read whole:

TEXT
def reconcile(tier):
    desired = ceil(active_users(tier) / users_per_instance[tier])
    while running(tier) < desired:
        start_worker(tier)                                # scale out
    while running(tier) > desired:
        victim = idle_worker(tier) or consolidate(tier)   # reclaim idle, else drain the least-loaded
        if not victim: break                              # nothing can be reclaimed safely
        stop_worker(victim)                               # scale in
    return desired, running(tier)
stateDiagram-v2
    [*] --> Observe
    Observe --> Compute: active users + policy
    Compute --> Compare: desired = ceil(active / users_per_instance)
    Compare --> ScaleOut: running < desired
    Compare --> ScaleIn: running > desired
    Compare --> Settled: running == desired
    ScaleOut --> Compare: start a worker
    ScaleIn --> Compare: reclaim idle, else consolidate
    Settled --> Observe: next event or tick

Three properties make this a control loop rather than a script:

Why is scaling in harder than scaling out?

Scaling out is trivial: a new worker has no users and is immediately useful. Scaling in must respect the people currently being served. The loop tries two moves, in order:

  1. Reclaim an idle worker. If any worker has no assigned users, stop it. Nobody notices.
  2. Consolidate. If every worker is busy but the fleet is still above desired โ€” which happens when the ratio is raised โ€” pick the least-loaded worker, move each of its users onto another worker that still has room under the ratio, and stop it. If even one user cannot be placed, roll back every move and stop trying. The fleet ends one worker above target rather than one user without a worker.

This worker consolidation is what makes "raise the ratio" a real scale-in, and the rollback rule is what makes it safe. Because workers are stateless, "moving a user" is only bookkeeping: the person's next turn lands elsewhere and pulls the same context pack.

โš ๏ธ Pitfall โ€” an autoscaler that kills the busiest worker because it was started first. Age is not load. Reclaim idle first, consolidate second, and never stop a worker whose users have nowhere to go.

๐Ÿ” See it happen โ€” three people, one policy edit, one departure

The ratio is set to 1 so every step is visible.

  1. Maya, Raj and Lena become active (three fictional personas). desired = ceil(3 / 1) = 3; running = 0. The loop scales out three times; least-loaded assignment puts one person per worker.
  2. An operator raises the ratio to 2. desired = ceil(3 / 2) = 2; running = 3. No worker is idle, so the loop consolidates: Lena's worker is least-loaded (tied, first by order); her one user moves to Maya's worker, which has room (1 < 2); Lena's worker stops. running = 2. Lena's next turn lands on the shared worker and is served identically.
  3. Raj goes quiet. He is unassigned first, then reconcile runs: desired = ceil(2 / 2) = 1; running = 2. Raj's old worker is idle and is reclaimed. running = 1, holding Maya and Lena.
  4. The ratio goes back to 1. desired = 2, running = 1 โ€” scale out by one; the next turn from the least-loaded person routes to the new worker.

Every step emitted a scale event with its reason, so the fleet's history reads like a ledger.

How is this the same as HPA and a managed autoscaler?

Exactly the same shape. The names change; the loop does not.

Concept This chapter Kubernetes HPA A managed autoscaler (e.g. Cloud Run)
Desired state desired = ceil(active / users_per_instance) desired = ceil(current_metric / target_metric ร— replicas) instances = f(concurrent requests / target concurrency)
Observation active users pod CPU, memory, or a custom metric in-flight requests per instance
Policy users per instance target utilization target concurrency, min and max instances
Actuator start or stop a worker create or delete a pod spin up or retire an instance
Scale-in safety reclaim idle, else consolidate with rollback stabilization window, graceful termination drain in-flight requests, then retire
Cadence every event plus a tick a sync period (seconds) continuous

Reading the first row across is the whole lesson: a ratio of work per worker against an observed amount of work. HPA writes that ratio with utilization; the managed autoscaler with concurrency; this chapter with people, because people are what a personal assistant is provisioned for. Anyone who has tuned one can tune the others.

What triggers scaling in production?

Here is the honest paragraph. "One user per instance" is a visualization. It is chosen so that one person joining makes one worker appear and a policy edit visibly consolidates the fleet โ€” the loop becomes something a reader can watch. Production runs the same loop with the ratio around ~100:1 โ€” stateless replicas each serving on the order of a hundred concurrent people โ€” and the observation is not a headcount but aggregate load: CPU, requests per second (QPS), and queue depth. The formula does not change; only the numerator and the policy do. Doing autoscaling on load rather than headcount matters because a hundred quiet people and ten intense ones cost the same machine very different amounts.

Three signals earn a place in the policy:

๐ŸŽฏ At consumer scale the fleet is not one fleet. Chat workers, retrieval workers, write-back workers and eval runners are separate tiers with separate ratios and triggers, each reconciled by the same loop. The control plane's real job is a policy per tier, so a burst of memory writes scales the write tier without touching the chat tier's bill.

From one person to a billion

Because workers are stateless and every per-user query is already scoped to a namespace, growth is "more replicas, more shards, more regions" โ€” not a redesign. The ladder:

Stage People What changes What stays the same
Seed 1โ€“10 one replica behind a managed autoscaler that scales to zero; a single relational store with its vector index and one hot cache the loop, the worker contract, the schema
Growth 10K the autoscaler scales on concurrency; connection pooling in front of the database; the hot cache absorbs profile and session reads โ€”
Scale 1M read replicas; a clustered hot cache; model routing and tiering to hold cost; the shared corpus moves to a dedicated vector service; edge caching for static assets โ€”
Massive 100M+ multi-region active-active; data sharded by user with region-pinned residency; every non-interactive step made asynchronous through queues; aggressive prefix caching โ€”
Planet 1B+ the same pattern with more shards and more regions; the bill is now dominated by model tokens, so routing, caching and tiering become the primary levers โ€”

Where it actually bottlenecks is never the stateless workers. It is (1) write throughput on the relational primary โ€” sharding by user; (2) model cost and latency โ€” model serving; (3) unscoped vector search over billions of documents โ€” dedicated vector infrastructure; and (4) per-user data residency โ€” region-pinned shards. None of the four touches the loop on this page.

๐Ÿงญ Enterprise mapping โ€” the reconcile loop is unchanged; the policy gains constraints. Users per instance becomes capacity per tenant with an SLO attached: a contract may guarantee minimum replicas so a tenant's p95 never depends on a neighbour's burst. Region pinning stops being an optimization and becomes a residency clause. Scale-in gains a change window ("never consolidate during the tenant's trading hours"). And every scale event โ€” already a ledger here โ€” becomes audit evidence that capacity was managed as promised. Same chassis, stricter policy.

Where next

The fleet keeps the right number of workers alive. What each worker spends per turn โ€” and why the bill is tokens, not machines โ€” is model serving. How a worker may act on the person's behalf is tools; how the loop is watched, gated and rolled out is quality; the reference architecture shows it running against a real managed autoscaler. If the ladder felt like a lot at once, start here.