<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: aln-1</title>
    <description>The latest articles on DEV Community by aln-1 (@aln-1).</description>
    <link>https://dev.to/aln-1</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4205187%2Fa31a7735-e421-49f7-be7c-7913d3080f78.png</url>
      <title>DEV Community: aln-1</title>
      <link>https://dev.to/aln-1</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aln-1"/>
    <language>en</language>
    <item>
      <title>What a PHP Framework Looks Like When It’s Designed for Persistent Workers and Non-Blocking I/O</title>
      <dc:creator>aln-1</dc:creator>
      <pubDate>Sat, 24 Oct 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/aln-1/what-a-php-framework-looks-like-when-its-designed-for-persistent-workers-and-non-blocking-io-364</link>
      <guid>https://dev.to/aln-1/what-a-php-framework-looks-like-when-its-designed-for-persistent-workers-and-non-blocking-io-364</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fipv5myzkk2yz9w0vuk4v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fipv5myzkk2yz9w0vuk4v.png" alt="Kinetis Framework" width="800" height="196"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you run PHP under FrankenPHP or RoadRunner, your framework’s state
model matters more than its router. This is how
&lt;a href="https://kinetis.dev/" rel="noopener noreferrer"&gt;Kinetis&lt;/a&gt; is built for persistent PHP workers
from the first line, and what each design decision costs and buys.&lt;/p&gt;

&lt;p&gt;PHP’s execution model is changing under the ecosystem’s feet.
FrankenPHP, Swoole, and RoadRunner keep a process alive across
requests: bootstrap once, stay warm, serve thousands. Most frameworks
meet this world through an adapter: the framework was designed for
boot-and-die, and a compatibility layer teaches it to survive
persistence. Kinetis starts from the opposite end, on two premises
that reinforce each other: &lt;strong&gt;persistent workers are the primary
deployment&lt;/strong&gt;, and &lt;strong&gt;I/O is non-blocking by design&lt;/strong&gt;, not as an
optional async add-on but as the framework’s native way of waiting.
One &lt;code&gt;&lt;span&gt;public/index.php&lt;/span&gt;&lt;/code&gt; runs under
&lt;a href="https://kinetis.dev/docs/runtime-adapters.html" rel="noopener noreferrer"&gt;FrankenPHP worker mode, RoadRunner, PHP-FPM and AWS Lambda&lt;/a&gt;;
FrankenPHP worker mode is the primary target. Concrete answers below,
each with the design decision it forced.&lt;/p&gt;

&lt;h2&gt;Request state in persistent PHP workers&lt;/h2&gt;

&lt;p&gt;Boot-and-die PHP had one glorious property: it was impossible to leak
state between requests, because the process died. Persistent workers
repeal that guarantee. A static property, a memoized “current user,” a
connection holding an open transaction: all of it now survives into
the next request, carrying one user’s state into another’s.&lt;/p&gt;

&lt;p&gt;Kinetis treats this as an architecture concern, not a code-review
concern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a href="https://kinetis.dev/docs/container.html" rel="noopener noreferrer"&gt;Two container scopes&lt;/a&gt;, enforced.&lt;/strong&gt;
Application scope holds what legitimately outlives requests (config,
pools, compiled caches); request scope is created per request and
disposed unconditionally.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Two PHPStan rules for your application code.&lt;/strong&gt;
&lt;a href="https://kinetis.dev/docs/container.html#nostaticpropertiesrule-the-enforcement-php-itself-can-t-provide" rel="noopener noreferrer"&gt;&lt;code&gt;&lt;span&gt;NoStaticPropertiesRule&lt;/span&gt;&lt;/code&gt;&lt;/a&gt;
flags every &lt;code&gt;&lt;span&gt;static&lt;/span&gt;&lt;/code&gt; property declaration, because a static is
exactly how request state slips past request scope. The default
answer to “can I stash this in a static?” is no.
&lt;a href="https://kinetis.dev/docs/concurrency.html#flagging-blocking-calls-with-phpstan" rel="noopener noreferrer"&gt;&lt;code&gt;&lt;span&gt;NoBlockingIoRule&lt;/span&gt;&lt;/code&gt;&lt;/a&gt;
flags blocking sleeps, sockets, curl waits and hand-built database
connections. Both are guardrails, not proofs, but they catch the
common mistakes before review does.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;&lt;span&gt;TransactionGuard&lt;/span&gt;&lt;/code&gt;&lt;/strong&gt; is the safety net for the sharpest leak: a
request that opens a database transaction and fails before closing
it. The disposal hook rolls back anything dangling, every request,
because connection pools have no concept of “your request ended.”
See &lt;a href="https://kinetis.dev/docs/persistence.html" rel="noopener noreferrer"&gt;persistence&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;Non-blocking I/O with Fibers and the Revolt event loop&lt;/h2&gt;

&lt;p&gt;Every waiting primitive in Kinetis is built on one pattern: register a
watcher on the Revolt event loop, suspend the current Fiber, resume
when the condition fires. Sockets, timers, the database drivers, the
Redis client, the HTTP client: different APIs, one loop, one way of
waiting.&lt;/p&gt;

&lt;p&gt;This is a stronger claim than “it has an async layer.” A framework
where async is an add-on keeps blocking calls in its core paths, and
a single blocking call inside a Fiber doesn’t become non-blocking. It
blocks the &lt;em&gt;entire worker&lt;/em&gt;, invisibly, defeating every concurrent task
sharing that thread. Designing non-blocking-first means the framework
refuses that failure mode structurally: the clients Kinetis ships or
adopts are chosen because they suspend Fibers on the same loop. Where
an extension gives no way to do that, the docs say so. A mysqli
connect blocks, because mysqli has no asynchronous connect, and so
does a libpq host lookup.
&lt;a href="https://kinetis.dev/docs/appendix-database.html#database-reference-native-io" rel="noopener noreferrer"&gt;The full list&lt;/a&gt;
is part of the database reference.&lt;/p&gt;

&lt;p&gt;The payoff is composability.
&lt;a href="https://kinetis.dev/docs/concurrency.html" rel="noopener noreferrer"&gt;&lt;code&gt;&lt;span&gt;concurrently()&lt;/span&gt;&lt;/code&gt;&lt;/a&gt; fans out
independent work on Fibers, and because every client waits the same
way, a single batch can mix a MySQL query, a Postgres query, a Redis
read, and an outbound HTTP call, and complete in roughly the
&lt;em&gt;slowest&lt;/em&gt; one’s time, not the sum. Given a connection pool as wide as
the fan-out, twenty queries cost one round trip of wall time. A
dashboard aggregating four backends costs its worst backend. No
worker-pool gymnastics, no promises API: closures in, results out,
in order.&lt;/p&gt;

&lt;p&gt;One boundary stated plainly, because eliding it is how async PHP gets
oversold: a worker thread (or a RoadRunner process) still handles one
HTTP request at a time. Cross-request concurrency comes from the
worker count, same as FPM’s; non-blocking I/O is what makes the
&lt;em&gt;inside&lt;/em&gt; of each request parallel. The two pillars divide the work.
Persistence amortizes what can be kept, and non-blocking I/O overlaps
what must be waited for.&lt;/p&gt;

&lt;p&gt;The subtle part is what a decade of thread-free PHP never had to
teach: Fibers on ZTS threads share a process address space, and
allocating a Fiber stack per task serializes every thread on the
kernel’s memory-map lock. Kinetis runs tasks on
&lt;a href="https://kinetis.dev/docs/appendix-runtime.html#resident-fibers" rel="noopener noreferrer"&gt;resident Fibers&lt;/a&gt;
that park between tasks instead of terminating, in a pool per thread
reused across tasks and requests. Persistence isn’t just “skip the
bootstrap”; it’s a license to keep &lt;em&gt;every&lt;/em&gt; expensive-to-create thing
alive, including the machinery of concurrency itself.&lt;/p&gt;

&lt;h2&gt;Match the database driver to the runtime, automatically&lt;/h2&gt;

&lt;p&gt;The same application code needs opposite database strategies in the
two runtimes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Under a persistent worker&lt;/strong&gt;: connections amortize across requests,
so pay for a real async client. MySQL queries run on native mysqli
connections from a per-thread pool and overlap with each other;
mysqli can’t hand its socket to the event loop, so the driver polls
it in short windows. Postgres goes further: &lt;code&gt;&lt;span&gt;pg_socket()&lt;/span&gt;&lt;/code&gt; exposes a
real file descriptor, so that driver is event-driven end to end,
connect included.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Under PHP-FPM&lt;/strong&gt;: every request pays a fresh connection and dies;
an async client buys nothing and costs CPU. The same contracts run
on plain lazy PDO, with one lesson learned the hard way. Native
prepares cost a server round trip each, so
&lt;a href="https://kinetis.dev/docs/appendix-database.html#prepared-statements-on-pdo" rel="noopener noreferrer"&gt;prepared statements are memoized per connection&lt;/a&gt;.
A twenty-query loop pays 21 round trips, not 40. That single cache
tripled the FPM deployment’s fan-out throughput.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Selection is
&lt;a href="https://kinetis.dev/docs/persistence.html#driver-selection" rel="noopener noreferrer"&gt;&lt;code&gt;&lt;span&gt;DB_DRIVER=auto&lt;/span&gt;&lt;/code&gt;&lt;/a&gt;:
the runtime is detected, the driver matches it, application code
doesn’t change. The split is measured, not aesthetic. Forcing either
driver into the other’s runtime measures worse.&lt;/p&gt;

&lt;h2&gt;Sizing connection pools for worker threads&lt;/h2&gt;

&lt;p&gt;Persistent workers multiply resources that FPM counted singly. Each
worker thread builds its own connection pool, so the real ceiling is
&lt;code&gt;&lt;span&gt;threads&lt;/span&gt; &lt;span&gt;×&lt;/span&gt; &lt;span&gt;maxConnections&lt;/span&gt;&lt;/code&gt; against the database’s &lt;code&gt;&lt;span&gt;max_connections&lt;/span&gt;&lt;/code&gt;.
That is &lt;a href="https://kinetis.dev/docs/performance-tuning.html#the-connection-budget" rel="noopener noreferrer"&gt;a budget&lt;/a&gt;,
not a knob. On 8 vCPUs, 20 worker threads × 12 connections = 240
against MySQL’s default of about 1,000 is the measured joint optimum
across read-heavy and write-heavy routes. Overshoot it and the surplus
connection is rejected mid-request, which makes it a correctness
problem, not a performance one. The sizing method
&lt;a href="https://kinetis.dev/docs/performance-tuning.html#tuning-by-workload-shape" rel="noopener noreferrer"&gt;differs by workload&lt;/a&gt;:
threads at 2 to 3× vCPUs and a wide pool for read fan-out, restraint
for writes, and the reasons why. The framework’s performance
documentation teaches the &lt;em&gt;interaction&lt;/em&gt;, because that’s the part no
database manual covers.&lt;/p&gt;

&lt;h2&gt;Compiling discovery ahead of time, without secrets&lt;/h2&gt;

&lt;p&gt;Persistent workers amortize application boot; compiled discovery
removes reflection from its hot path.
&lt;a href="https://kinetis.dev/docs/cli.html#kinetis-build" rel="noopener noreferrer"&gt;&lt;code&gt;&lt;span&gt;kinetis&lt;/span&gt; &lt;span&gt;build&lt;/span&gt;&lt;/code&gt;&lt;/a&gt;
compiles routes and validation plans, MCP tools and resources,
commands and event listeners into one plain PHP artifact loaded at
boot. It is useful under workers and transformative under FPM, where
it replaces per-request reflection. Publishing is all-or-nothing: a
failed compile never replaces a
&lt;a href="https://kinetis.dev/docs/caching.html" rel="noopener noreferrer"&gt;cache&lt;/a&gt; a previous build
produced.&lt;/p&gt;

&lt;p&gt;The build command never runs the application’s bootstrap, so
pre-warming the artifact in a pipeline requires no database
credentials, no API keys, nothing the application’s services demand.
&lt;code&gt;&lt;span&gt;#[Command(bootstrap:&lt;/span&gt; &lt;span&gt;false)]&lt;/span&gt;&lt;/code&gt; opts any static-shape command of your
own out of application boot the same way. Cache compilation with
production secrets absent is table stakes for real pipelines, and it’s
easy to get wrong when the framework assumes “boot the app” is free.&lt;/p&gt;

&lt;h2&gt;The same application under PHP-FPM&lt;/h2&gt;

&lt;p&gt;Everything above degrades deliberately. Under PHP-FPM the same
application runs unmodified, with request scope per request, PDO
drivers, compiled caches, and &lt;code&gt;&lt;span&gt;concurrently()&lt;/span&gt;&lt;/code&gt; returning correct
results with sequential execution. The
&lt;a href="https://kinetis.dev/docs/benchmarks.html" rel="noopener noreferrer"&gt;benchmark suite&lt;/a&gt; runs
Kinetis both ways precisely to keep that promise measurable. On
FrankenPHP worker mode, Kinetis
&lt;a href="https://kinetis.dev/docs/benchmarks.html#who-leads-each-test" rel="noopener noreferrer"&gt;leads five of six&lt;/a&gt;
TechEmpower-style tests against eight other PHP frameworks, and loses
&lt;code&gt;&lt;span&gt;/updates&lt;/span&gt;&lt;/code&gt; to Slim. On PHP-FPM, the identical application still leads
every full framework on &lt;code&gt;&lt;span&gt;/json&lt;/span&gt;&lt;/code&gt;, &lt;code&gt;&lt;span&gt;/plaintext&lt;/span&gt;&lt;/code&gt; and &lt;code&gt;&lt;span&gt;/db&lt;/span&gt;&lt;/code&gt; and is level
on &lt;code&gt;&lt;span&gt;/fortunes&lt;/span&gt;&lt;/code&gt;; it gives ground only on &lt;code&gt;&lt;span&gt;/queries&lt;/span&gt;&lt;/code&gt; and &lt;code&gt;&lt;span&gt;/updates&lt;/span&gt;&lt;/code&gt;,
where a blocking driver has nothing to overlap. The rig, every
implementation and the fairness rules are public in the
&lt;a href="https://github.com/aln-1/kinetis-benchmarks" rel="noopener noreferrer"&gt;benchmark repository&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;The position&lt;/h2&gt;

&lt;p&gt;PHP’s persistent-worker era doesn’t need faster adapters bolted onto
boot-and-die designs. It needs the assumptions inverted: state
lifetimes enforced by scopes, waiting done by suspending Fibers
instead of blocking threads, drivers chosen by runtime, resources
budgeted per thread, compilation ahead of time. Kinetis is one answer:
a non-blocking core in a persistent runtime, built in the open, with
an auditable benchmark, a documented tuning method, and its worst test
result printed in the same table as its best.&lt;/p&gt;

&lt;p&gt;To try it, start with the &lt;a href="https://kinetis.dev/docs/tutorial.html" rel="noopener noreferrer"&gt;tutorial&lt;/a&gt;
or the &lt;a href="https://kinetis.dev/docs/" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;.&lt;/p&gt;




</description>
      <category>php</category>
      <category>fibers</category>
      <category>frankenphp</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
