<p>A few weeks ago I wrote about the thesis behind Askr: what if the app just stayed booted? We embedded PHP inside a Rust server, booted Laravel once, and served it from a memory-safe binary at ~9× the FPM model. That was the foundation.</p><p>This release is what happens when you sit with that foundation for a while and start noticing — almost embarrassingly — that a whole pile of things the PHP world normally bolts on from the outside are already inside the building. There's a shared-memory substrate all the worker processes see. There's a copy-on-write template that respawns a warm app in milliseconds. And Askr owns the entire request lifecycle, start to finish.</p><p>So this release is seven features that didn't require new infrastructure. They just fell out of the architecture. Let me walk you through them the way I found them.</p><h2>1. A response cache with an instant kill switch</h2><p>Here's the one I'm most fond of. Most Laravel pages that anonymous visitors see are the same for everyone — a blog index, a product page, a marketing route. FPM re-renders them on every hit. The usual fix is Varnish or a Redis cache layer: more infrastructure, more moving parts, and the eternal question "how do I bust the cache when a post changes?"</p><p>Askr already has a shared-memory table every worker can see. So the cache lives in the server. The app opts a response in with a header:</p><pre><code class="language-php">// in a controller, only on pages you know are safe to cache
return response($html)-&gt;header('Askr-Cache', '60, tags=posts,homepage');
</code></pre><p>The next anonymous GET for that URL is served straight from Rust — PHP is never touched. It runs at static-file speed. You can watch it happen:</p><pre><code class="language-text">$ curl -sD- localhost/posts | grep -i x-askr-cache
x-askr-cache: MISS          # first hit: PHP ran, response stored

$ curl -sD- localhost/posts | grep -i x-askr-cache
x-askr-cache: HIT           # second hit: never reached PHP
</code></pre><p>But the part that makes it feel different is invalidation. Every cached entry remembers its tags. When your data changes, you bump the tag:</p><pre><code class="language-php">class Post extends Model {
    protected static function booted() {
        static::saved(fn () =&gt; askr_cache_forget_tag('posts'));
    }
}
</code></pre><p>That call increments a generation counter in the shared tag table. Every cached response carrying posts — across every worker process — goes stale at that instant. O(1). No scan, no cache-server round-trip, no coordination. It's the Varnish effect, driven by your app, with nothing to install.</p><p>(Two safety rails, because caching auth'd content is how you leak someone's session to the world: only cookie-less GET/HEAD requests are cacheable, and Set-Cookie is stripped on store. The app opts in; Askr keeps it honest.)</p><h2>2. …and it never stampedes</h2><p>Once the cache existed, the classic failure mode was one line away: a cold cache, and 200 identical requests arrive at once. Everyone misses, everyone runs PHP, your database melts.</p><p>Because all the workers share memory, the fix is easy: the first request to miss becomes the leader and runs PHP; the other 199 wait for it to fill the cache, then get served from it. I tested it with 20 simultaneous requests to a 300 ms endpoint on a cold cache:</p><pre><code class="language-text">20 concurrent cold-cache requests → 346 ms total   (not ~6 s)
PHP ran exactly once.  coalesced: 19
</code></pre><p>Twenty requests, one execution. Stampede gone. You didn't configure anything — it's automatic whenever the response cache is on.</p><h2>3. The Reverb you didn't install</h2><p>Askr already had a broadcast ring feeding Server-Sent Events. But the Laravel ecosystem — Echo, Livewire — speaks the Pusher protocol. So this release teaches Askr to speak it too: a WebSocket endpoint at /app/{key} and the HTTP trigger POST /apps/{id}/events that Laravel's broadcaster calls server-side.</p><p>Point Echo at Askr, change nothing else, and it works. Here's the whole loop, verified with a real WebSocket client:</p><pre><code class="language-text">→ connect        ws://localhost/app/localkey
← {"event":"pusher:connection_established", ...}
→ {"event":"pusher:subscribe","data":{"channel":"orders"}}
← {"event":"pusher_internal:subscription_succeeded","channel":"orders",...}

# meanwhile, server-side, Laravel broadcasts an event:
$ curl -XPOST localhost/apps/1/events -d '{"name":"OrderCreated","channel":"orders","data":"..."}'

← {"event":"OrderCreated","channel":"orders","data":"{\"id\":42}"}
</code></pre><p>A trigger in any worker reaches subscribers in all of them, because it rides the same shared ring. That's the whole real-time story — no Reverb, no external broker, one binary. (Public channels are complete; private/presence auth-signature verification is the next step, and I'd rather ship it honestly than pretend.)</p><h2>4. Work that happens after "send"</h2><p>You know the feeling: the response is ready, but you still have to fire off a welcome email, hit a webhook, write an audit log — and the user is sitting there watching a spinner while you do it.</p><p>Since Askr controls the whole lifecycle, it can hand the response to the client first, then do the rest:</p><pre><code class="language-php">askr_defer(function () use ($user) {
    Mail::to($user)-&gt;send(new Welcome);   // runs after the client already left
});
</code></pre><p>Rust flushes the reply; the worker runs your deferred closures before it accepts the next request. Octane calls this defer() but needs its own machinery — here it's just there. I proved it to myself the dumb, satisfying way: the client got its response in 18 ms, and the "slow work" file appeared 50 ms after that.</p><h2>5. Workers that breed under load — and get harvested</h2><p>This one has never been practical in PHP, and it bugged me for years. Autoscaling processes is the natural way to handle a traffic spike, but PHP boot is ~300 ms, so spinning up a worker mid-spike is like starting a car by pushing it uphill.</p><p>Except Askr has the CoW template: it boots the app once and forks warm workers in ~35 ms. Suddenly the arithmetic flips. So the template now watches a live queue-depth gauge in shared memory and sizes the pool itself:</p><pre><code class="language-bash">askr serve --cow --worker-script … --workers-min 2 --workers-max 12
</code></pre><p>Under load it adds warm workers; when the rush passes it harvests them back down. Straight from the log of a test run:</p><pre><code class="language-text">cow autoscale up    busy=10 alive=1 → desired=2
cow autoscale up    busy=18 alive=2 → desired=3
cow autoscale up    busy=23 alive=3 → desired=4
…
cow autoscale down (harvest)  busy=0 → 3
cow autoscale down (harvest)  busy=0 → 2
cow autoscale down (harvest)  busy=0 → 1
</code></pre><p>No other PHP server can do this cheaply, and it's purely because respawn is warm.</p><h2>6. Replaying production, instead of guessing</h2><p>A 500 in production is the worst debugging loop: you have a stack trace, maybe, and then you spend an hour trying to reproduce the exact request. Askr sees every request in full, so it can just… keep the bad ones:</p><pre><code class="language-bash">askr serve … --record-errors /var/lib/askr/errors
</code></pre><p>Every 5xx writes its whole CGI envelope — method, URI, the full $_SERVER, the raw body. Later:</p><pre><code class="language-text">$ askr replay /var/lib/askr/errors/1783232551-89972-0.json
↻ replaying … → POST
HTTP 500
X-Boom: yes

boom uri=/boom?debug=1 body=payload=42
</code></pre><p>Same method, same query, same headers, same body, against a fresh interpreter. Debugging goes from "try to reproduce" to "replay it." (It captures request bodies, so treat that directory as sensitive — but you already knew that.)</p><h2>7. A test runner that boots once</h2><p>Last one, and the most fun to build. paratest speeds up your suite by running test files in parallel processes — but every one of those processes pays the full cold boot. With a warm CoW template, you don't have to:</p><pre><code class="language-bash">askr test --root /path/to/app --runner examples/askr-test.php tests/
</code></pre><p>Boot the app once; fork a fresh, warm process per test file. You get perfect isolation — no state bleeds between files, because each is its own process — and no cold boot per file. My little proof:</p><pre><code class="language-text">✓ IsolationTest.php      # a global set in another file? not visible here.
✗ FailTest.php (exit 1)
✓ PassTest.php

3 file(s): 2 passed, 1 failed
</code></pre><p>Point --runner at the included PHPUnit shim and it drives Pest/PHPUnit per file. (Building it taught me a lovely little truth about the embed SAPI, too: exit(0) unwinds as a Zend bailout, so I was briefly reporting every clean exit as a failure until I learned to trust EG(exit_status) over the bailout code.)</p><h2>The thread running through all of it</h2><p>None of these are clever tricks. They're the same three ideas paying off over and over:</p><ul><li><p>Shared memory across processes → the cache, its tag table, the coalescing lock, the broadcast ring, the autoscaler's gauge. No IPC, no broker.</p></li><li><p>Copy-on-write respawn in milliseconds → autoscaling becomes cheap, and so does the test runner.</p></li><li><p>Owning the whole request lifecycle → defer after send, record on 5xx, cache before PHP even wakes up.</p></li></ul><p>The whole release is honest about its edges — private-channel auth, warm-autoload in the test runner, and the big one still ahead: the per-core io_uring I/O core and a real benchmark against FrankenPHP and FPM. That's where "the most efficient PHP server" stops being a claim and becomes a number.</p><p>But the shape of the thing is clear now. Boot once, share memory, respawn warm — and a startling amount of the infrastructure the PHP world usually assembles from separate boxes just… collapses into one binary you can curl down and run.</p><p>If you run Laravel, it's one command to try:</p><pre><code class="language-bash">VER=v0.3.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 serve --root /var/www/app/public \
  --worker-script examples/laravel-worker.php \
  --response-cache 512 --pusher --workers "$(nproc)"
</code></pre><p>Cache a page, forget a tag, watch a worker get born. Then tell me what breaks. 🌳</p><p>Askr is MIT-licensed, built by Knut W. Horne / Wirelabs AS. The name is the ash tree the gods carved the first human from — the thing that came alive and faced the world. Its dev-tool sibling is Grove.</p>