Askr · · 6 min read

I finally built the thing I'd been sketching on napkins for years - Askr v0.8.2

How Askr — a Rust application server that embeds PHP in-process — became the most efficient way I know to run Laravel: no FPM, no nginx, no Redis, no certbot. Just one binary.

I finally built the thing I'd been sketching on napkins for years - Askr v0.8.2

How Askr became — I'll say it out loud — the most efficient way to run Laravel and PHP.

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.

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.

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.

Why it's so fast: the whole idea in one breath

Traditional PHP hosting is a relay race with a lot of handoffs:

browser → nginx → (FastCGI socket) → PHP-FPM → boot Laravel → run → tear down

Every request re-bootstraps the framework and serializes a request across a socket. Askr deletes the handoffs:

browser → askr (Rust, in-process PHP, app booted once) → run → respond
  • 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.

  • The app boots once (worker mode, the Octane model) and serves thousands of requests — no per-request bootstrap.

  • 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 fork(). Simple, robust, no thread-safety tax.

  • The hot path is memory-safe Rust. Static files, compression, TLS, routing — all in Rust. The only unsafe is the thin FFI seam where PHP lives.

  • OPcache + JIT on PHP 8.5, warm in every worker.

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.

The journey from 0.3.1 to 0.8.2

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.

🔌 Killing Redis (0.6.0 – 0.6.1)

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.

// Cache, counters, atomic locks — no Redis
askr_cache_set('user:1', $json, ttl: 60);
askr_cache_increment('hits');
Cache::lock('import')->get(fn () => …);   // 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);

# 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

On a single box, that's cache + sessions + locks + pub/sub + queues — Redis, gone.

📡 Real-time without Reverb (0.3.1, broadcasting)

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.

askr_broadcast('orders', ['id' => 42, 'status' => 'shipped']);
askr serve --pusher --pusher-secret "$ASKR_PUSHER_SECRET"   # drop-in Reverb

📤 The un-sexy essentials done right (0.4.0 – 0.5.2)

The stuff that decides whether a server is a toy or a tool:

  • 0.4.0 — streaming multipart uploads. Files stream to disk (constant memory), show up as real $_FILES / $request->file() even in worker mode.

  • 0.4.1 — compression + observability. Brotli/gzip negotiated in Rust, a structured JSON access log, and Prometheus /metrics (PHP-vs-I/O time split, latency histogram, per-worker RSS).

  • 0.4.2 — Docker + cgroup awareness. A multi-arch image on GHCR, and --workers auto reads the container's CPU limit, not the host's 64 cores.

  • 0.5.0 — the full extension set. intl, gd, curl, zip, pdo_mysql/pgsql… enough to run Filament, Livewire/Flux, and Inertia unchanged.

  • 0.5.1 — the humbling one. A 0-byte static file was sent with Content-Length: 1, quietly breaking Vite's CSS-only entry. One-line fix, big lesson: the boring paths matter.

  • 0.5.2 — supervised sidecars. Run anything alongside the workers and have it respawned — e.g. Inertia SSR:

[[sidecar]]
command = "node bootstrap/ssr/ssr.mjs"

🔒 One binary that replaces the whole stack (0.7.0 – 0.8.0)

  • 0.7.0 — automatic TLS. Askr obtains and renews a Let's Encrypt cert itself over HTTP-01. No certbot, no proxy. The master answers challenges on :80 before forking; a background thread rolls workers when the cert nears expiry.

askr serve --acme --acme-domain example.com --acme-email you@example.com
  • 0.8.0 — hardening. --sandbox shrinks the blast radius of a PHP exploit. seccomp makes execve return EPERM (no shell from an RCE); Landlock lets the worker read everywhere but write only where you allow (no webshell in your docroot).

askr serve --sandbox --sandbox-write /var/www/app/storage --sandbox-write /tmp

I verified this in a Linux container: shell_exec("id") → blocked, write to /tmp → ok, write into the docroot → denied, normal pages → unchanged.

🚀 Operability (0.8.1) and the latest engine (0.8.2)

  • 0.8.1 — askr upgrade. 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.

sudo askr upgrade --restart        # fetch latest, verify, swap, restart
sudo askr upgrade --version 0.8.0  # …or roll back
  • 0.8.2 — PHP 8.5, tuned for Laravel 13. 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: --enable-opcache 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 --disable-zend-signals). A fresh Laravel 13.18.1 now boots and serves 200/200 under load with a clean shutdown.

So what does running it actually look like?

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:

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

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 sudo askr upgrade --restart when it's time to move on.

The quiet satisfaction

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.

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.

Now, about that io_uring core… 🌳