Skip to content

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.

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).

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.

Terminal window
falak-ctl status # PHP threads: panel 1/8 busy · agent-api 5/128 busy
falak-ctl doctor # flags a pool that is >= 80 % busy or saturated

A saturated agent-api pool delays agents, not the panel.

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).

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.

Set FALAK_WORKER_MODE=0 in .env, run falak-ctl up, and report the issue. The panel then boots Laravel for every request.