<p><em>How Askr became — I'll say it out loud — the most efficient way to run Laravel and PHP.</em></p><p>For years I had a diagram in the back of my head. It followed me through side projects and long train rides: a PHP application server where PHP lives inside the process, not on the other end of a FastCGI socket. No nginx forwarding to PHP-FPM forwarding to a pool of workers that re-bootstrap Laravel on every single request. No Redis box for cache. No certbot cron. Just… one binary that is your app server.</p><p>I kept telling myself it couldn't be that simple, or someone would already have done it in a way I'd want to use. But the idea wouldn't leave. So a while back I finally started typing instead of sketching, and the result is Askr — a standalone PHP application server written in Rust that embeds PHP in-process and scales by forking one worker per core.</p><p>And somewhere around version 0.8.2, sipping coffee and watching a fresh Laravel 13 app serve 200/200 requests with a clean shutdown, it clicked: the theory holds. This really is the most efficient server available for Laravel/PHP that I know of. Let me tell you why, and how, with real examples.</p><h2>Why it's so fast: the whole idea in one breath</h2><p>Traditional PHP hosting is a relay race with a lot of handoffs:</p><pre><code class="language-text">browser → nginx → (FastCGI socket) → PHP-FPM → boot Laravel → run → tear down
</code></pre><p>Every request re-bootstraps the framework and serializes a request across a socket. Askr deletes the handoffs:</p><pre><code class="language-text">browser → askr (Rust, in-process PHP, app booted once) → run → respond
</code></pre><ul><li><p>PHP is embedded in the process. No FastCGI, no FPM, no socket serialization. The Rust HTTP layer hands a request straight to a warm interpreter.</p></li><li><p>The app boots once (worker mode, the Octane model) and serves thousands of requests — no per-request bootstrap.</p></li><li><p>We scale by processes, not threads. PHP is non-ZTS, so instead of fighting that, we lean into it: one worker process per core, sharing a listen socket via <code>fork()</code>. Simple, robust, no thread-safety tax.</p></li><li><p>The hot path is memory-safe Rust. Static files, compression, TLS, routing — all in Rust. The only <code>unsafe</code> is the thin FFI seam where PHP lives.</p></li><li><p>OPcache + JIT on PHP 8.5, warm in every worker.</p></li></ul><p>The result is that a request that used to cost a full framework boot now costs… running your controller. That's the whole trick. Everything below is me making that trick bulletproof and complete.</p><h2>The journey from 0.3.1 to 0.8.2</h2><p>Here's the fun part — the features that turned "neat spike" into "I'd actually run my company on this." Grouped by why, with the how.</p><h3>🔌 Killing Redis (0.6.0 – 0.6.1)</h3><p>The single biggest "wait, do I even need that box?" moment. Because all workers are forked from one master, I could put a shared-memory substrate (an mmap'd region created before the fork) under everything: cache, counters, locks, sessions, and a job queue. Length-clamped reads make it memory-safe even under races.</p><pre><code class="language-php">// Cache, counters, atomic locks — no Redis
askr_cache_set('user:1', $json, ttl: 60);
askr_cache_increment('hits');
Cache::lock('import')-&gt;get(fn () =&gt; …);   // backed by askr_cache_add (set-if-absent)

// Sessions: just point Laravel at it
// SESSION_STORE=askr

// A real job queue, in the binary
askr_queue_push('emails', $payload, delay: 30);
</code></pre><pre><code class="language-toml"># askr.toml
[cache]
slots = 4096          # counters, locks, small values
large_slots = 512     # sessions, cached fragments (64 KB each)

[queue]
slots = 8192          # delayed jobs, reserve/visibility timeout, retries
</code></pre><p>On a single box, that's cache + sessions + locks + pub/sub + queues — Redis, gone.</p><h3>📡 Real-time without Reverb (0.3.1, broadcasting)</h3><p>0.3.1 finished the Pusher-compatible WebSocket with proper private/presence auth (HMAC-SHA256), so Laravel Echo just works — plus a plain SSE endpoint for the simple cases.</p><pre><code class="language-php">askr_broadcast('orders', ['id' =&gt; 42, 'status' =&gt; 'shipped']);
</code></pre><pre><code class="language-bash">askr serve --pusher --pusher-secret "$ASKR_PUSHER_SECRET"   # drop-in Reverb
</code></pre><h3>📤 The un-sexy essentials done right (0.4.0 – 0.5.2)</h3><p>The stuff that decides whether a server is a toy or a tool:</p><ul><li><p><strong>0.4.0 — streaming multipart uploads.</strong> Files stream to disk (constant memory), show up as real <code>$_FILES</code> / <code>$request-&gt;file()</code> even in worker mode.</p></li><li><p><strong>0.4.1 — compression + observability.</strong> Brotli/gzip negotiated in Rust, a structured JSON access log, and Prometheus <code>/metrics</code> (PHP-vs-I/O time split, latency histogram, per-worker RSS).</p></li><li><p><strong>0.4.2 — Docker + cgroup awareness.</strong> A multi-arch image on GHCR, and <code>--workers auto</code> reads the container's CPU limit, not the host's 64 cores.</p></li><li><p><strong>0.5.0 — the full extension set.</strong> intl, gd, curl, zip, pdo_mysql/pgsql… enough to run Filament, Livewire/Flux, and Inertia unchanged.</p></li><li><p><strong>0.5.1 — the humbling one.</strong> A 0-byte static file was sent with <code>Content-Length: 1</code>, quietly breaking Vite's CSS-only entry. One-line fix, big lesson: the boring paths matter.</p></li><li><p><strong>0.5.2 — supervised sidecars.</strong> Run anything alongside the workers and have it respawned — e.g. Inertia SSR:</p></li></ul><pre><code class="language-toml">[[sidecar]]
command = "node bootstrap/ssr/ssr.mjs"
</code></pre><h3>🔒 One binary that replaces the whole stack (0.7.0 – 0.8.0)</h3><ul><li><p><strong>0.7.0 — automatic TLS.</strong> Askr obtains and renews a Let's Encrypt cert itself over HTTP-01. No certbot, no proxy. The master answers challenges on <code>:80</code> before forking; a background thread rolls workers when the cert nears expiry.</p></li></ul><pre><code class="language-bash">askr serve --acme --acme-domain example.com --acme-email you@example.com
</code></pre><ul><li><p><strong>0.8.0 — hardening.</strong> <code>--sandbox</code> shrinks the blast radius of a PHP exploit. seccomp makes <code>execve</code> return <code>EPERM</code> (no shell from an RCE); Landlock lets the worker read everywhere but write only where you allow (no webshell in your docroot).</p></li></ul><pre><code class="language-bash">askr serve --sandbox --sandbox-write /var/www/app/storage --sandbox-write /tmp
</code></pre><p>I verified this in a Linux container: <code>shell_exec("id")</code> → blocked, write to <code>/tmp</code> → ok, write into the docroot → denied, normal pages → unchanged.</p><h3>🚀 Operability (0.8.1) and the latest engine (0.8.2)</h3><ul><li><p><strong>0.8.1 — </strong><code>askr upgrade</code><strong>.</strong> Since an install is a directory (binary + bundled libphp), upgrading downloads the right tarball, verifies its sha256, and swaps the whole prefix atomically — keeping the old one for rollback.</p></li></ul><pre><code class="language-bash">sudo askr upgrade --restart        # fetch latest, verify, swap, restart
sudo askr upgrade --version 0.8.0  # …or roll back
</code></pre><ul><li><p><strong>0.8.2 — PHP 8.5, tuned for Laravel 13.</strong> The newest engine, with OPcache compiled into libphp and JIT on by default. Along the way I caught two real 8.5 gremlins by actually running it: <code>--enable-opcache</code> was removed (OPcache is built in now), and PHP 8.5's signal handling chained with Rust's into an infinite loop at shutdown (fixed by letting the host own signals with <code>--disable-zend-signals</code>). A fresh Laravel 13.18.1 now boots and serves 200/200 under load with a clean shutdown.</p></li></ul><h2>So what does running it actually look like?</h2><p>This is the part I still grin at. One command, and you've replaced app server + nginx + PHP-FPM + Redis + a queue worker + a cron + certbot:</p><pre><code class="language-bash">askr serve \
  --config askr.toml \
  --worker-script examples/laravel-worker.php \
  --workers auto \
  --acme --acme-domain example.com --acme-email you@example.com \
  --sandbox --sandbox-write /var/www/app/storage --sandbox-write /tmp
</code></pre><p>One process tree. In-process PHP with OPcache + JIT. Cache, sessions, locks, queues, and broadcasting in shared memory. Auto-TLS. Seccomp + Landlock around the PHP boundary. And <code>sudo askr upgrade --restart</code> when it's time to move on.</p><h2>The quiet satisfaction</h2><p>The thing I keep coming back to isn't a benchmark number (though the per-request savings are enormous when you skip the framework boot). It's that the whole mental model got simpler. Fewer moving parts, fewer boxes, fewer 2 a.m. "which layer is broken" moments — and a memory-safe Rust shell around the one unsafe thing (PHP) that we then wrapped in seccomp and Landlock for good measure.</p><p>A theory I doodled for years turned out to be true: if you stop making PHP shout across sockets and just invite it inside, you get something remarkably fast, remarkably small, and — dare I say — kind of cozy to operate.</p><p>Now, about that io_uring core… 🌳</p>