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:
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.
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.
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.
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)
Three properties make this a control loop rather than a script:
Idempotent. Running it twice changes nothing the second time, so every event can trigger it
without coordination.
Bidirectional. It scales out and in. A loop that only adds is a leak with a good excuse.
Convergent. Each pass moves running one step toward desired, stopping when they meet or no safe
move remains.
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:
Reclaim an idle worker. If any worker has no assigned users, stop it. Nobody notices.
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.
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.
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.
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.
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.
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:
CPU / memory โ the classic HPA target; catches expensive turns (long context, many tool calls).
QPS per replica โ catches bursts before CPU does, because a queued request is not yet burning CPU.
Queue depth โ the truthful signal for the async fleets (write-back, eval): a growing backlog means
add workers, even though no person is waiting.
๐ฏ 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.