# Performance tuning

> Tune the Falak control plane — FrankenPHP worker mode, panel and agent-api PHP thread pools, Horizon and memory — with measured numbers and sizing rules.

Source: https://falak.sh/docs/operations/performance/

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

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

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

```bash
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.

## 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

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

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

The 0.2.x hotfix `FRANKENPHP_CONFIG=num_threads 24` in `custom.env` still overrides automatic sizing. Remove it; see [Upgrade](/docs/operations/upgrade/#the-php-thread-hotfix).
