Scale beyond one host¶
The shipped path is one host with Docker Compose. This page is for the step after it: a managed Postgres, several API hosts, workers of their own, or several deployments side by side. It says what each part needs when it moves, and what the platform does not do yet, so a plan is built on what runs.
Measure first. One host of the recommended size carries a team of a few dozen, and most slowness on a small host is CPU steal or an undersized pool rather than a missing replica. The load suite runs the same workload against any deployment.
What holds state, and what does not¶
| Part | What it keeps | How it grows |
|---|---|---|
API (app) |
Nothing of its own. Sessions, presence, running chat turns and the live-update feed are in Postgres and Redis | More uvicorn workers, then more hosts behind a load balancer |
| Worker | Nothing. Jobs, leases and schedules are rows in Postgres | More processes, on any host, with the pools split between them |
Console (frontend) |
The branding it caches for five minutes | More instances |
| Postgres | Everything durable: organizations, agents, conversations, vectors, the job queue | A bigger instance, or a managed one |
| Redis (Valkey) | A cache and its pub/sub channels, rebuilt after a restart | One instance that every host shares |
| Uploaded files | A volume on the host, or a bucket | A bucket once more than one host reads or writes them |
| Sandbox service | The sandboxes agents run commands in | Its own host, see Agent workspaces |
One database per deployment¶
Every organization on a deployment shares one Postgres database, and every
query is scoped by organization. The vectors of every knowledge base are in it
too, through pgvector, and so is the job queue. POSTGRES_DB names the only
database a deployment uses: there is no separate vector database, no read
replica and no sharding by organization.
Several databases therefore means several deployments. That is the answer for
data that must stay in one region, for a business unit with administrators of
its own, and for a tenant whose load should not reach the others. Each
deployment has its own hostnames, SECRET_KEY and VAULT_MASTER_KEY, and
nothing is shared between them at runtime.
They can share infrastructure. Two deployments on one Postgres server need
their own databases (POSTGRES_DB), and on one Redis their own REDIS_DB
numbers, so that rate-limit buckets and cached answers stay apart. A bucket is
one per deployment as well. An agent moves between them as its spec: export it
as YAML on one (GET /agents/{id}/spec.yaml), and import it into an agent on
the other.
An external Postgres¶
A managed or self-run Postgres works when it has these:
- PostgreSQL 16 with pgvector. Managed services list it as the
vectorextension. The platform creates it the first time a collection is written to, so the connecting role needsCREATEon the database, or an administrator creates it beforehand. The same applies topg_stat_statements, which a migration adds where it may and skips with a notice where it may not. - An encrypted connection for a database on another host. See
Encrypted connections: the
override has to reach
app,migrateandworker, because the compose files pin the host in each of them. - Migrations run once. The
migrateservice applies them before the API starts. With several hosts, run it from one deploy rather than from each.
After a managed failover, the first request in each process that meets a dropped connection fails, and the pool then discards every connection older than it. The engines do not ping a connection before using it, because that would cost every checkout a round trip.
Counting connections¶
Size max_connections before anything else, or Postgres refuses connections with
"too many clients" before a pool can queue or shed the load.
Each API process opens two pools of DB_POOL_SIZE + DB_MAX_OVERFLOW, 15 each
by default: one for requests and one for vector searches, which run inside a
request that already holds a connection. A worker process opens the same two,
plus a short-lived connection for each job it is running. Each ingest child is a
process of its own, and the job it runs writes its vectors through an engine of
its own with the default pool of 15, closed when the job ends. Knowledge
searches made by an agent connect once per search, without a pool.
max_connections ≥ API hosts × UVICORN_WORKERS × 30
+ worker processes × 30
+ ingest children × 15
+ headroom for migrations and an administrator's session
Two API hosts with four uvicorn workers each and two workers with one ingest child each come to 330, so 400 is a sensible setting. These are ceilings: an idle deployment holds far fewer. The production compose file sets 200 for one host.
PgBouncer¶
Only in session mode. Transaction pooling breaks two things the platform relies
on: asyncpg's prepared statements, which no setting here turns off, and the
session-level advisory lock that keeps one sync source from running twice at
once. Session mode caps how many server connections exist but saves little
else, so sizing the pools against max_connections is usually the better
lever.
More API hosts¶
Put a load balancer in front that passes WebSockets: the console's live updates
and chat use /api/v1/ws/*. Sticky sessions are not needed. A person whose
socket reconnects to another host follows a running turn through Redis, and
presence and rate limits are kept there too.
Every host needs:
- The same secrets.
SECRET_KEYsigns tokens that any host must verify, andVAULT_MASTER_KEYdecrypts every stored credential. - The bucket. With
FILE_STORAGE_BACKEND=local, a file uploaded through one host is not on the other, and the worker that parses it may be on neither. Uses3(see Uploaded files at rest). - The same Redis, as one instance or one endpoint in front of a primary. The client is not cluster-aware, so Redis Cluster is not supported.
RATE_LIMIT_TRUST_FORWARDED_FORonce the load balancer is the only thing that reaches the hosts, or every visitor shares the balancer's bucket.
A chat turn runs in the API process that accepted it, not in a worker, so the
API hosts carry the chat load and UVICORN_WORKERS is sized for it as
on one host.
Several console instances are fine. The branding they cache is per instance: an administrator's change shows at once on the instance that saved it and within five minutes on the others.
More workers¶
Run as many worker processes as the work needs, on any hosts. They claim jobs
with SKIP LOCKED, so no two run the same job, and any of them fires a due
schedule. A worker that dies gives its jobs back within WORKER_LEASE_SECONDS.
Why the queue is rows in Postgres, measured against nine other engines, is on
the background-job benchmark.
Split the pools when parsing competes with agent runs. WORKER_POOLS=agent,system
on one set of processes and WORKER_POOLS=ingest on hosts with memory to spare
keep a large upload from delaying a routine. The health probe and
agenticos cmd doctor report a pool that no live worker serves. The
Background work settings size each process.
Redis¶
Every API host and worker uses the same Redis: it carries the live-update feed, presence, running turns and rate limits between them. It is a cache, so a restart loses nothing durable. Live updates reconnect, and a person following a running turn sees the answer in the transcript when it lands rather than frame by frame.
Measure before you add hosts¶
agenticos cmd doctorsays which Postgres and Redis each connection reached, whether it was encrypted, and whether the workers are alive.pg_stat_statementsshows which statements cost the database most.- Logfire, with
LOGFIRE_ENVIRONMENTset per deployment, separates one deployment's traces from another's (see Observability). - The load suite, run before and after a change, says whether it helped.
What it does not do yet¶
- Read replicas, or a vector database of its own.
- Sharding organizations across databases within one deployment.
- PgBouncer in transaction mode, or Redis Cluster.
- Kubernetes manifests. The images are the same ones the compose files run, and each service carries its own health check, so a cluster runs them with the environment this page describes.