<p>It started, like most of my side projects do, with a small itch that wouldn't go away.</p><p>Every PHP request throws away the world and rebuilds it. Autoloader, service providers, the container, config, middleware — all of it, on every single request. FPM has done this faithfully for a decade, and it works. But somewhere in the back of my head a voice kept asking: what if the app just… stayed booted?</p><p>FrankenPHP and Laravel Octane already answered part of that. But FrankenPHP is built on Caddy and inherits Go's garbage collector, and Octane needs an external runtime. I wanted something else: one memory-safe binary, PHP running in-process, and the app booted exactly once. So one weekend I opened an empty folder and started a spike.</p><p>This is the story of that spike growing into Askr.</p><h2>Why bother?</h2><p>Four costs hide inside "serving PHP," and I wanted to drive all of them toward zero at the same time:</p><ol><li><p>Bootstrap per request — the ~110 ms of framework boot Laravel pays every time.</p></li><li><p>The IPC hop — nginx → FastCGI → FPM → back again.</p></li><li><p>GC pauses — the tax you pay with Go-based servers.</p></li><li><p>Isolation that costs — you either pay in memory (prefork) or in state bleed (worker mode).</p></li></ol><p>The insight that made it worth doing: the HTTP throughput isn't the bottleneck — Rust laughs at that. The bottleneck is that PHP boots too much, too often. Fix that, keep it memory-safe, and put the whole thing in one binary, and you've got something genuinely new for the Laravel world.</p><h2>How it works, in three ideas</h2><h3>1. PHP lives inside the Rust process</h3><p>Askr links PHP's embed SAPI (libphp) directly. No FastCGI, no FPM pool — the Zend engine runs in-process. The first time I saw this work was a genuine "oh, it's real" moment:</p><pre><code class="language-text">$ cargo run -p askr-php --example hello
embedded PHP version: 8.4.11
hello from PHP 8.4.11
{ "stack": "TALL", "server": "askr", "n": 55 }
</code></pre><p>All the unsafe in the whole project is confined to that thin FFI boundary. Everything else — the accept loop, TLS, the supervisor — is safe Rust. PHP is the single unsafe frontier, and it's the only C we own (a ~200-line shim).</p><h3>2. It scales by processes, not threads</h3><p>Here's a subtlety that shaped the entire architecture. Askr uses a non-ZTS build of PHP — no thread-safety tax. But non-ZTS means one interpreter per process; you literally cannot run two in one process.</p><p>So Askr doesn't spin up threads. It runs one worker process per CPU core, each with its own interpreter, all accepting on a shared socket. That is the share-nothing, thread-per-core philosophy — realised with processes. No locks on the hot path, nothing shared that shouldn't be.</p><h3>3. Worker mode: boot once, serve many</h3><p>This is where it gets fun. Instead of running index.php from scratch every time, a tiny worker script boots the app once and then loops:</p><pre><code class="language-php">$app = require $base . '/bootstrap/app.php';
$kernel = $app-&gt;make(Kernel::class);

while (askr_handle_request(function (array $request) use ($kernel) {
    $req = Illuminate\Http\Request::create(
        $request['uri'], $request['method'], [], $cookies, [], $server, $request['body']
    );
    $response = $kernel-&gt;handle($req);
    http_response_code($response-&gt;getStatusCode());
    foreach ($response-&gt;headers-&gt;all() as $n =&gt; $vs) foreach ($vs as $v) header("$n: $v", false);
    echo $response-&gt;getContent();
    $kernel-&gt;terminate($req, $response);
})) {}
</code></pre><p>askr_handle_request blocks until Rust hands it a request, runs your handler against the already-booted app, and ships the response back — all in-process, no IPC.</p><p>The numbers, on a real Laravel 12 + Livewire app:</p><p>per-request (the FPM model) worker mode (boot once) latency / request ~110 ms ~9 ms throughput (8 workers) 37 req/s 347 req/s</p><p>The very first request costs 303 ms (cold boot). The second? 9.9 ms. The third, 8.9 ms. That gap — that's the whole thesis, sitting right there in the logs.</p><h2>The fun parts (a.k.a. what nobody tells you)</h2><p>Building this taught me things I'll never un-know:</p><ul><li><p>STDERR doesn't exist in the embed SAPI. It's a CLI-only constant. My first test scripts kept fataling on fwrite(STDERR, ...) and I blamed everything except the one thing that was wrong.</p></li><li><p>The extension matrix is real. Booting Laravel surfaced exactly what it needs, one fatal at a time: mb_split() → oniguruma. openssl_cipher_iv_length() → OpenSSL. Class "DOMDocument" not found → libxml2. Each one built from source, statically. The macOS SDK's libxml2 was even too old for PHP 8.4's ext/dom.</p></li><li><p>Laravel boots its providers on the first request, not when you build the kernel. That one cost me an evening on the state-bleed detector.</p></li><li><p>Forking a booted interpreter is safe if you fork while single-threaded. The CoW template boots PHP on the main thread and forks before tokio starts — no multi-threaded-fork hazard. It felt like cheating when it worked.</p></li></ul><h2>What you actually get</h2><p>Askr grew a personality. A few of my favourite bits:</p><p><strong>"Is my app worker-safe?"</strong> — the #1 fear with the Octane model is state leaking between requests. So Askr will just tell you:</p><pre><code class="language-text">$ askr serve --worker-script worker.php --workers 1 --paranoid
[askr paranoid] baseline set after 2 requests — watching 95 app classes
[askr paranoid] request #42 — state changed after reset (possible bleed):
  ↑ App\Services\Foo::$cache  array:2 → array:3  (+1)
</code></pre><p>Silence means clean. That growing arrow means you have a leak, and exactly where.</p><p><strong>Cache, without Redis.</strong> A shared-memory cache lives in the binary — askr_cache_* from PHP, atomic counters for rate limiting, and a drop-in Laravel driver:</p><pre><code class="language-php">Cache::increment('hits');                         // atomic across all workers
Cache::remember('report', 600, fn () =&gt; build()); // computed once, shared
</code></pre><p><strong>Live updates, without Reverb.</strong> PHP publishes, Rust holds the connections:</p><pre><code class="language-php">askr_broadcast('orders', json_encode($order));   // from anywhere in PHP
</code></pre><pre><code class="language-js">new EventSource('/askr/events?channel=orders').onmessage = e =&gt; update(e.data);
</code></pre><p><strong>Zero-bad-deploy.</strong> A reload rolls one worker first, health-checks it, and only rolls the rest if it stays healthy:</p><pre><code class="language-text">INFO  canary healthy — rolling the rest
ERROR canary UNHEALTHY — aborting reload; remaining workers keep old code
</code></pre><p>A broken deploy takes down one worker instead of your whole fleet.</p><p>And ~35 ms warm respawns via the CoW template — recycled workers are re-forked from the still-booted app instead of cold-booting.</p><h2>Try it (it's honestly one command)</h2><p>There's a self-contained release for Linux — x86_64 and arm64 — with the binary, embedded PHP, and everything else in one tarball:</p><pre><code class="language-bash">VER=v0.2.0; ARCH=$(uname -m)
curl -fsSLO https://github.com/kwhorne/askr/releases/download/$VER/askr-${VER#v}-linux-$ARCH.tar.gz
tar xzf askr-${VER#v}-linux-$ARCH.tar.gz &amp;&amp; cd askr-${VER#v}-linux-$ARCH

./askr-run.sh doctor
ASKR_APP_BASE=/var/www/app ./askr-run.sh serve \
  --root /var/www/app/public \
  --worker-script examples/laravel-worker.php \
  --workers "$(nproc)" --tls-self-signed
</code></pre><p>No Rust, no build, no libphp compilation. There's a full production setup guide with a hardened systemd unit, Let's Encrypt, and recommended settings when you're ready for real traffic.</p><h2>Where it's headed (and where it isn't)</h2><p>Let me be honest, because that's the fun of a side project: it's early. The I/O layer is still tokio/epoll — the per-core io_uring core is the next big step, and it's where "the most efficient PHP server" gets measured against FrankenPHP and FPM. HTTP/3, real WebSockets, and a proper askr-laravel package are on the list too.</p><p>But the thesis holds. Real Laravel, booted once, served from a memory-safe Rust binary, ~9× the FPM model — with a state-bleed detector, a shared cache, broadcasting, queue workers and the scheduler all living in one file.</p><p>I set out to answer a small, nagging question — what if the app just stayed booted? — and ended up with a server I actually want to run.</p><p>If you run Laravel, give it a spin. And if it segfaults on your box, well — that's what the Ubuntu test is for. 🌳</p><p><em>Askr is MIT-licensed and built by Knut W. Horne / Wirelabs AS. The name is the ash tree the gods carved the first human from — the thing that became alive and faced the world. Its dev-tool sibling is Grove: the grove where trees grow.</em></p>