Benchmarks

2026-10-05 — PHP path profiling and fix

Environment as in 2026-10-05-m2-runtimes.md (Docker Desktop smoke host, scb on CPUs 0-3, PHP-FPM pm = static with 4 workers on 4-5, oha on 6-11, 64 connections).

Method

Throughput alone was too noisy on this host (nginx + the same pool measured 10.5k–20.5k req/s across three identical runs), so the decisive metric was server CPU per request from perf stat plus an inclusive profile (scripts/profile.sh --docker php).

Isolation experiment (same CPU placement): nginx + external pool 16.5k; scb + the same external pool 15.1k; scb + its managed pool 15.6k, with equal PHP-FPM CPU per request in all three. So the pool configuration was not the cause; PHP execution dominates and the difference was scb’s own per-request cost.

Findings (before)

scb spent ~150 µs CPU per PHP request (nginx ~91 µs). Inclusive profile:

CostShare of scb CPU
FastCGI connection per request: close 7.7 %, connect 4.6 %, socket 3.7 %, epoll_ctl 3.5 %~20 %
Routing plan: two openat2 + fstat + close per request (directory, then index.php)~10 %

Fix

  1. Persistent FastCGI connections (FCGI_KEEP_CONN) for managed pools, bounded by a semaphore to the pool’s max_children (a kept-alive connection pins an FPM worker). Idle connections are checked for staleness before reuse; replayable requests (no body or a buffered body ≤ 64 KiB) are retried once on a fresh connection if a reused one fails before any response byte; streaming bodies always use a fresh connection. External pools keep one connection per request (other clients may share them).
  2. Routing plan cache: 2 s TTL, 4096 entries (like nginx’s open_file_cache).
  3. Buffered writes to the FastCGI socket.

A new acceptance test drives sequential and concurrent requests (with POST bodies) through a pool with max_requests: 7, i.e. constant worker recycling; it caught a real bug in the first version (non-replayable POST on a stale connection → 502).

After

beforeafternginx
scb CPU per request (profile run)150 µs99 µs~91 µs
req/s (profile run)15.6k20.4k—
bench run 1 (scb / nginx / Caddy)—21.8k / 21.8k / 13.9k
bench run 2—21.8k / 23.4k / 14.6k

scb is now at 0.93–1.0× nginx on PHP (was 0.70×), with p99 5.3 ms and 16 MiB peak RSS (nginx 56 MiB).

On LSAPI

The data shows the remaining cost is PHP execution itself; protocol choice is not the bottleneck. What LSAPI is usually credited for — persistent connections and less per-request setup — is now done over open FastCGI. An LSAPI adapter stays a possible optional adapter (handoff §5) but is not needed for performance.