Production¶
How to choose hosts, scale out, and size an ApiPi process. Production
is apipi serve --api-only plus apipi worker on KVM. Combined
apipi serve is one box. Why that split exists is in
workers. Guest internals are in
isolation.
Host selection¶
Run production as apipi serve --api-only plus apipi worker on
KVM hosts with APIPI_RUN_MODE=microvm. Combined apipi serve is the
single-host embedded worker. Nested Docker or nested KVM is a lab
setup. Chat without a computer uses a second worker pool with
APIPI_RUN_MODE=chat on the same API-only gateway. See
chat fleets. The Compose file in this repo starts Postgres (and can run a
rootless API). systemctl stop / restart on apipi worker sends
SIGTERM. The worker heartbeats "drain": true (no new leases), waits
until live Pi are gone, then exits 0. Install
deploy/systemd/apipi-worker-drain.conf as a drop-in so
TimeoutStopSec covers that wait. Expired leases fail
closed; they are not reassigned. Set APIPI_METRICS and
APIPI_OTEL_ENDPOINT on the worker as well as the API so turn
series and turn/model spans are recorded where the turn runs. See
usage.
Isolation none is for local machines and CI. If the selected mode
cannot start, apipi serve exits before it binds HTTP. The process
never switches to another mode on its own.
Size the box from live sessions, not from Postgres row counts.
Each live session is one Pi process or Firecracker guest. Guest RAM is
the real cost (APIPI_MICROVM_MEM_MIB, default 512). Firecracker VMM
overhead is small (~5 MiB per guest). Pi alone is modest. Playwright
or Chromium inside the guest needs hundreds of MiB extra, so raise
guest RAM rather than packing more 512 MiB guests.
Leave disk for APIPI_SESSIONS_DIR: each openai_hosted directory is
capped at APIPI_MAX_WORKSPACE_BYTES (default 1 GiB) and lasts until
sandbox TTL. Local artifact
bytes add up to APIPI_MAX_ARTIFACT_BYTES (default 512 MiB) per
session unless you set APIPI_ARTIFACT_STORE=s3.
Keeping the store off the worker host leaves more RAM for guests when
you use Postgres. One apipi worker (or one combined apipi serve)
per sandbox host; extra uvicorn workers leave the Pi pool in the first
worker only. One process can use SQLite. Several API processes share
Postgres. Give each process its own SQLite file if you are not sharing.
Store¶
| Need | Why Postgres |
|---|---|
| Several gateway processes or nodes | SQLite is a single-writer file; share Postgres |
| Many concurrent writers on one DB | Single writer / lock |
| HA, backups, pooling at scale | Operator story |
Guest image store¶
Build images with apipi images build, then publish them with
apipi images publish --to s3://your-bucket/apipi-images or
file:///path on storage the workers can read. An https:// static
host is a valid source once the files are there; publish itself
rejects https://. Point workers at that prefix with one image
source setting. The official Images workflow can publish default
and browser as GitHub release assets. S3 credentials are the AWS
environment or the instance role, the same endpoint settings as the
artifact store, and never a key in TOML.
Scale-out¶
| Shape | When | What stays on the node | What is shared |
|---|---|---|---|
| Combined, one host | You fit in max_sessions on one box |
Pi, SSE, WebSockets, openai_hosted directories, local artifacts |
SQLite or Postgres |
| API-only + workers | Production. API in Docker or several replicas | Guests and workspaces on workers. API is stateless for Pi | Postgres, worker token. Artifact bytes too when APIPI_ARTIFACT_STORE=s3 |
| Combined, several hosts | You have not split workers yet | Same as combined one host, plus each process has its own APIPI_SESSIONS_DIR |
Postgres, auth callback. Sticky for live Pi. See multiple nodes |
| External artifact store | Clients read artifacts from any API node | Live workspace still on the worker (or combined node) | Postgres, S3-compatible bucket |
With workers, POST /v1/agents/sessions and follow-up REST/SSE may
land on any API replica. The session is owned by the worker lease.
There is no live handoff of a running guest. Combined serve still
needs sticky routing for Pi. Examples are in
multiple nodes.
Sizing¶
Count live Pi processes (or guests) on workers. The API
process is cheap next to guest RAM. Idle TTL (default 15 minutes)
kills the Pi process group on host workers (none / chat) and
frees that RAM. The session row can outlive the process.
max_sessions and worker_memory_mb do not count Postgres rows.
worker_memory_mb ≈ (worker RAM − reserve) # MiB, advertised as memory_mb
max_sessions ≈ worker_memory_mb / microvm_mem_mib
Reserve several GiB on each worker for the OS, jailer, and page cache.
Colocated Postgres needs more. Set APIPI_WORKER_MEMORY_MB from
reserved guest RAM rather than average guest RSS. The scheduler
will not start a guest that would pass either the RAM budget or
max_sessions. A new turn that cannot lease returns 429 with code
capacity.
Example: 64 GiB RAM, 12 cores¶
Postgres on another host. Worker APIPI_RUN_MODE=microvm. Default
guest RAM 512 MiB and 1 vCPU. The API can be a small VM or a
container.
| Reserve | About 8 GiB for OS, gateway, jailer, and page cache. |
| RAM budget | worker_memory_mb=57344 (56 GiB). This is what the worker advertises as memory_mb. |
| Default 32 live | 32 × 512 MiB ≈ 16 GiB guests plus ~0.2 GiB VMM. Fits easily. |
| Starting cap | max_sessions=48 (24 GiB guests) or keep 32. Raise after you watch host RSS and 429 capacity. The RAM cap still applies. |
| Ceiling | 57344 / 512 ≈ 112 live at 512 MiB. That is the wall, not a starting point. |
| Playwright / Chromium | Use sandbox size L so the guest gets the browser rootfs, about 2 GiB RAM, and Playwright MCP against system Chromium. Then about 24–28 live L guests in a 56 GiB budget. S (512 MiB) is for Pi and light tools. |
| CPU | 48 × 1 vCPU on 12 cores is normal while turns wait on the model URL. Keep APIPI_MICROVM_VCPUS=1 unless the computer is CPU-heavy. |
| Disk | Hosted workspaces last until sandbox TTL (default 1 hour), capped at 1 GiB each. Local artifacts 512 MiB per session unless S3. Worst case is cap × live-and-idle directories, not typical use. |
| NIC | Each guest TAP is 50 Mbit. 48 guests all saturated ≈ 2.4 Gbit. That is the ceiling, not the plan. |
On a shared node set APIPI_MAX_SESSIONS_PER_TENANT lower than the
node cap (for example 8). A tenant that would pass it gets 429 with
code capacity_tenant.
self_hosted still costs a Pi guest on this host. The runner disk is
elsewhere and is not capped here.
Overprovision¶
| Resource | Overprovision? | Why |
|---|---|---|
| Guest RAM | No. Set worker_memory_mb so the sum of guest mem_mib plus the host reserve fits. Size max_sessions as a second hard cap. |
Each live session is a Firecracker guest with that RAM. There is no balloon device. The guest kernel usually touches the memory. The scheduler will not oversubscribe RAM or session count. |
| CPU | Yes. Default 1 vCPU per guest. | Turns mostly wait on the model URL. Watch host load, not the vCPU count. Raise microvm_vcpus only if the computer is CPU-heavy (builds, Playwright). |
| Disk | Caps are maxima, not reservations. | max_workspace_bytes and max_artifact_bytes are per session. Summing them is worst case. Hosted workspaces last until sandbox TTL. Provision for typical use and alert before the disk fills; a burst can still hit the caps. S3 moves artifact bytes off the node. |
| NIC | Same as disk. | Each TAP is capped at 50 Mbit. All guests saturating at once is unlikely. |
| Stored sessions | Yes, by design. | Postgres rows are not live Pi. Idle TTL frees RAM; the thread stays. Many stored sessions on one 64 GiB box is fine. Only live guests count. |
On the 64 GiB example, 48 × 512 MiB ≈ 24 GiB guests on about 56 GiB usable is not RAM overprovision. Packing toward 110 live would leave no headroom.
Logs¶
Ship stderr. There is no log file shipper in the gateway. apipi serve
writes one JSON object per line. Default level is info. Use
APIPI_LOG_FORMAT=text only on a laptop.
Info covers process start (version, bind, run mode, store), one line
per HTTP request except /health and /metrics, and turn completed
or cancelled. Failed turns, sandbox boot failures, unexpected
exceptions, and HTTP 5xx are error. Warnings are degraded-but-running
(SQLite one-process, run_mode=none, capacity, lease expiry, export
drop). Debug is optional diagnosis.
Error and warning lines that operators should alert on include
event and error_code. Same id fields as traces when known
(request_id, session_id, turn_id, tenant_id, worker_id). The
event table is in usage. Keep secrets and prompt
bodies out of the logs. How to scrape metrics, ship logs, and point
OTLP at a collector is in observability.
Tuning¶
Change a setting and restart the process. There is no plan or SKU field. Details and defaults are in configuration.
| Setting | Why it matters |
|---|---|
APIPI_RUN_MODE |
Set microvm for Firecracker production isolation. Nested TOML is [sandbox].backend. See configuration. |
APIPI_MAX_SESSIONS |
Live Pi on this node. Hard cap (429 capacity). |
APIPI_MAX_SESSIONS_PER_TENANT |
Live Pi for one tenant (429 capacity_tenant). |
APIPI_MICROVM_MEM_MIB / APIPI_MICROVM_VCPUS |
Guest RAM and vCPUs. Raise RAM for Playwright. Keep 1 vCPU unless the computer is CPU-heavy. |
APIPI_IDLE_TTL |
Kill idle Pi for none and self_hosted (default 15 minutes) and free a live slot. Hosted computers use sandbox TTL. |
APIPI_SANDBOX_TTL_OPENAI_HOSTED |
Stop hosted Pi and delete the workspace (default 1 hour). |
APIPI_TURN_TIMEOUT |
Cancel a stuck turn (default 10 minutes). |
APIPI_DB_POOL_SIZE |
Postgres connections from this process (default 5). |
APIPI_MAX_REQUEST_BYTES |
HTTP body cap (413 payload_too_large). |
APIPI_MAX_WORKSPACE_BYTES / APIPI_MAX_ARTIFACT_BYTES |
Directory and published-artifact caps. |
APIPI_ARTIFACT_STORE |
local or s3. Use s3 when more than one node serves artifact, hosted file, or skill bytes. |
APIPI_MICROVM_EGRESS_ALLOWLIST / HOSTS / MBIT |
Optional destination allowlist (off by default) and 50 Mbit TAP rate. Private IPv4 ranges are always rejected. |
APIPI_INSTANCE_ID |
Sets X-ApiPi-Instance so you can confirm stickiness. |
APIPI_VAULT_MASTER_KEY |
Encrypts MCP vault tokens at rest. Put a 32-byte key in the process environment or a k8s secret. Unset uses a local default and logs a warning; do not leave that in production. Same key on every API process that writes or injects vault secrets. |
Tenant-aware deployments¶
Most tenants share one gateway pool. Some tenants get their own pool:
a separate Host name or load-balancer backend group. Auth still
returns tenant_id. You map that tenant to the pool outside the
gateway. The public Agents API does not change.
Isolated in a dedicated pool: Pi and APIPI_SESSIONS_DIR. Shared
across pools: Postgres, and object-store bytes (artifacts, hosted files,
skills) when the store is s3.
Sticky rules still apply inside the pool. See
tenant pools.
Failure and drain¶
Health checks should call GET /health. Probe health rather than a
session. That endpoint accepts requests without a bearer.
To drain a worker, systemctl stop (or restart) it. SIGTERM sets
heartbeat "drain": true, idle Pi exit, in-flight turns finish, then
the process exits 0. Use the drain drop-in so TimeoutStopSec is
longer than --drain-timeout. A timeout exits 1; systemd then SIGKILLs
the cgroup (KillMode=control-group). Keep API health successful while
a turn is in flight. A live session stays on the node that owns it.
Host workers (none / chat) stamp Pi and host stdio MCP with
APIPI_WORKER_PID. After a crash, the next apipi worker or combined
apipi serve start reaps processes whose stamped parent is dead. It
does not kill another live worker's Pi, and it does not match on the
pi command name. systemd units must set KillMode=control-group so
systemctl stop kills the unit cgroup, including Pi. A raw
kill -9 of the worker PID does not; the startup sweep covers that.
Checklist after kill -9 of a host worker: start the worker again,
then confirm no leftover processes remain whose APIPI_WORKER_PID
is the old worker PID.
If SSE drops, reconnect with after_seq to replay from the store. The
next turn still needs the node that holds Pi.
A full node returns 429 with code capacity. A tenant at its cap
returns 429 with code capacity_tenant. Clients should retry later;
idle reap on the process that holds Pi frees a slot (the worker, when
the API is --api-only). Request bodies over APIPI_MAX_REQUEST_BYTES
return 413.