Performance tuning
The defaults suit a 2–8 CPU host with up to about 120 servers. This page explains the knobs and when to turn them.
Two PHP pools
Section titled “Two PHP pools”The web tier is two FrankenPHP services built from the same image, each with its own PHP thread pool:
| Service | Serves | PHP mode | Threads |
|---|---|---|---|
control-plane |
The panel and the REST API (everything below excluded) | Worker mode: Laravel boots once per thread (Octane’s FrankenPHP worker) | FALAK_PHP_WORKERS workers (default 2 × CPUs), autoscaled up to FALAK_PHP_MAX_THREADS (default max(8, 4 × CPUs)) |
agent-api |
/agent/* (agents), /install/* (installer), /api/internal/* (builders, artifacts) |
Classic (boot per request) | 32 started, autoscaled up to FALAK_AGENT_API_THREADS (default 128) |
Why the split: every agent and builder holds a 30-second long-poll open all the time, and a waiting long-poll occupies a PHP thread. Up to v0.2.x the panel and agents shared FrankenPHP’s default pool of 2 × CPUs threads; on a 2-vCPU host three servers and the builder took all four, and pages queued for 5–11 seconds. Now the panel’s threads serve only people. A waiting long-poll costs about 2–3 MB and no CPU, and holds no database connection (agents wait on Valkey).
Sizing
Section titled “Sizing”Set in /opt/falak/.env, then falak-ctl up:
| Knob | Rule |
|---|---|
FALAK_AGENT_API_THREADS |
At least managed servers + builders + 8. 128 covers about 120 servers within agent-api’s 512 MB limit. For bigger fleets raise it together with that limit (about 3 MB per thread). |
FALAK_PHP_WORKERS, FALAK_PHP_MAX_THREADS |
Defaults suit 2–8 CPUs. Each panel worker keeps a booted app (about 12 MB). |
FALAK_WORKER_MODE=0 |
Fallback to classic mode for the panel: 2–4× slower per request, no state kept between requests. FALAK_PHP_THREADS then sets the starting thread count. |
FALAK_HORIZON_MAX_PROCESSES |
Default 4 (auto-balanced from 1). Lower it on small hosts. |
Each container logs its pool at start, for example falak: web: PHP worker mode, 4 workers, num_threads 6 max_threads 8.
Watching the pools
Section titled “Watching the pools”falak-ctl status # PHP threads: panel 1/8 busy · agent-api 5/128 busyfalak-ctl doctor # flags a pool that is >= 80 % busy or saturatedA saturated agent-api pool delays agents, not the panel.
Measured
Section titled “Measured”Production image, 2 pinned CPUs, with Postgres and Valkey; panel TTFB is the median of 20 requests. “Before” is v0.2.0 (one shared pool of 4 threads).
| before, idle | before, 4 long-polls | before, 10 long-polls | after, idle | after, 50 long-polls | |
|---|---|---|---|---|---|
/up |
5.4 ms | 7.6 s | timeout (> 15 s) | 1.8 ms | 2.7 ms |
/login |
11 ms | 11.1 s | timeout | 3.9 ms | 3.5 ms |
/servers |
32 ms | 11.1 s | timeout | 16.6 ms | 16.2 ms |
| project canvas | 38 ms | 11.0 s | timeout | 23 ms | 23 ms |
Throughput with 16 concurrent clients: /login 212 → 978 req/s and the canvas 66 → 119 req/s (classic → worker mode).
The image also caches config, routes, events and views at start, uses an authoritative Composer classmap, and runs OPcache without timestamp checks (4 MB realpath cache). CLI processes (Horizon, Reverb, scheduler) use an OPcache file cache: an artisan boot drops from 86 to 40 ms. JIT is off (tracing JIT measured within noise, +15 MB).
Memory
Section titled “Memory”The core stack idles at 0.6–0.8 GB, with container limits totalling about 4.3 GB. Observability adds about 0.6 GB idle. On a 2-vCPU / 4 GB host the core stack leaves more than 3 GB free; with observability, plan for 8 GB so builds have room. If memory is tight, lower FALAK_HORIZON_MAX_PROCESSES or move observability to its own host.
When something breaks only in worker mode
Section titled “When something breaks only in worker mode”Set FALAK_WORKER_MODE=0 in .env, run falak-ctl up, and report the issue. The panel then boots Laravel for every request.