Achieving Single‑Thread Peak Performance: How TrueAsync Server Rewrites the PHP Server Landscape in C
TrueAsync Server is a high‑performance HTTP/1.1, HTTP/2 and HTTP/3 server written in C that embeds directly into the PHP process, runs entirely on a single thread, eliminates inter‑thread communication, supports multi‑protocol on one port, and offers a suite of low‑level optimizations and a clear API for developers.
TrueAsync Server
TrueAsync 0.7.0 adds a thread pool and other features. The most notable component is TrueAsync Server, a high‑performance HTTP/1.1, HTTP/2 and HTTP/3 server embedded directly into PHP, requiring no separate process or reverse proxy.
Everything Runs in One Thread
TrueAsync Server follows a "everything runs in one thread" model: the full request lifecycle—from parsing to sending the response—executes on a single thread. It is implemented as a native extension rather than in PHP itself (Swoole also runs a single worker in its basic mode). AMPHP uses a similar single‑thread event‑loop model, but AMPHP is written in PHP while TrueAsync Server is a native extension embedded in the PHP process.
The "one thread per event loop" model is common to NGINX, Envoy, Node.js and the Rust Tokio + hyper stack. A single thread owns the connection and request from start to finish, eliminating hand‑offs, locks and context switches.
Reference: HTTP Arena leaderboard https://www.http-arena.com/leaderboard/?v=composite&res=mem
Pros and Cons
A major drawback: if the PHP VM and TrueAsync Server share the same thread and the PHP VM crashes, the server worker crashes as well, causing the client to lose the connection abruptly. Placing the reactor and PHP VM in different threads or processes would make the architecture more robust, allowing the client to receive an error instead.
Advantages remain:
No inter‑thread communication. This avoids complex algorithms that are never optimal for all workloads; some network loads perform well, others do not.
Simple, predictable scaling. Starting a second worker roughly doubles performance. Workers are started via setWorkers(N) and the kernel distributes connections with SO_REUSEPORT. Each worker has its own event loop, no shared state, no global lock.
Full, unconstrained control of the server. The PHP VM and server form a single entity. Managing connections in another thread would be far more complex; with everything in one thread many decisions become simpler. Multi‑worker mode mitigates the crash drawback: a worker crash does not bring down other workers.
Why Use C Instead of PHP
TrueAsync Server is written in C for several reasons:
Embedding the server directly into PHP is the most convenient way to stay as close as possible to the language.
It leverages proven C libraries: nghttp2 for HTTP/2, ngtcp2 + nghttp3 for HTTP/3, and llhttp (the same parser used by Node.js) for HTTP/1.1.
The server links OpenSSL directly, which is already part of the PHP build. For HTTP/3, OpenSSL 3.5+ is required because the QUIC TLS API first appears there.
The server runs on the Zend VM, giving better resource control (memory is accounted under a single memory_limit) but also inheriting some performance penalties of the Zend VM.
The server parses data structures directly into PHP arrays.
Single Port, Multiple Protocols
Multiple protocols share a single TCP port and event loop: HTTP/1.1, HTTP/2, WebSocket, SSE and gRPC. Protocol selection is done via ALPN (for TLS) or HTTP Upgrade. HTTP/3 runs on the same UDP port using QUIC and advertises the Alt‑Svc header so clients can switch transparently. A single $server->start() call can serve REST APIs over HTTP/2, push events via Server‑Sent Events, keep WebSocket connections alive, and expose gRPC endpoints.
Server Optimizations
High throughput is the sum of many small decisions:
Pooling on the hot path. Body buffers, compression encoders, HTTP/3 streams, connection slots—all are pooled to avoid allocator and kernel interference on repeated requests.
Geometric growth of large buffers. PHP's smart_str has a hidden cliff: beyond a threshold each growth triggers a system call, costing up to half the request time for large bodies.
Zero‑copy on the hot path. The multipart parser works directly on the incoming buffer. HTTP/2 serves static content without an intermediate PHP buffer; HTTP/1 falls back to sendfile().
Kernel‑friendly networking. SO_REUSEPORT distributes connections among workers, merges headers with response bodies, and matches block size to TLS record size.
Shared memory between concurrent requests. If one request opens a file, its buffer can be reused by another request. The server is embedded in the TrueAsync event loop, so when PHP code awaits a DB response the server can accept the next request. Awaiting I/O yields the coroutine, allowing the reactor to pick the next ready event without idle threads.
API Overview
The public API consists of two core classes: HttpServerConfig – configuration object. HttpServer – the server itself, created from the config and started.
A minimal application:
use TrueAsync\HttpServer;
use TrueAsync\HttpServerConfig;
$server = new HttpServer(
(new HttpServerConfig())
->addListener('0.0.0.0', 8080)
);
$server->addHttpHandler(function ($request, $response) {
$response->setStatusCode(200)->setBody('Hello, World!');
});
$server->start(); // blocks the thread until stop() is calledListeners
Listeners combine "protocol + transport + host + port". addListener() – TCP, HTTP/1.1 + HTTP/2 (chosen by first byte or ALPN). addHttp1Listener() / addHttp2Listener() – port limited to a single protocol. addHttp3Listener() – UDP/QUIC. addUnixListener() – Unix domain socket.
Processors
Processors are functions invoked for each new request, receiving request and response objects and operating as usual. The server supports multiple processors, each matching a specific protocol:
$server->addHttpHandler(fn ($req, $res) => {/* ... */}); // HTTP/1.1 + HTTP/2
$server->addHttp2Handler(fn ($req, $res) => {/* ... */}); // HTTP/2 only
$server->addWebSocketHandler(fn ($req, $res) => {/* ... */});Each processor runs in its own coroutine: HTTP/1 gets one coroutine per request, HTTP/2 and HTTP/3 get one coroutine per stream. When a processor await s (e.g., a DB call) it does not block other connections or streams.
Request and Response
Request objects are read‑only. Their API includes getMethod(), getUri(), getHttpVersion(), getHeader(), getHeaderLine(), getHeaders(), hasHeader(), getContentType(), getContentLength(), getBody(), and for forms/uploads getPost(), getFiles(), getFile().
Response objects are the sole output channel, using a fluent interface:
$response
->setStatusCode(200)
->setHeader('Content-Type', 'text/plain')
->setBody('payload')
->end();Streaming
Because the server supports HTTP/2 and HTTP/3, it is built for streaming from the start. Developers control when data is sent without waiting for the processor to finish, and the server does not force the entire response to be buffered.
Buffered – setBody() / write() accumulate the body and send it once.
Streaming – send() pushes each chunk directly to the wire.
For the first send(), the server sends the appropriate headers ( Transfer‑Encoding: chunked for HTTP/1.1, DATA frames for HTTP/2/3), so the same handler code works across protocols.
Request bodies can also be streamed using Request::readBody(), allowing partial reads (e.g., 64 KiB chunks) without waiting for the full upload, enabling gigabyte‑size uploads to be proxied to a file or another service without full in‑memory assembly.
while (($chunk = $req->readBody()) !== null) {
$sink->write($chunk); // process 64 KiB at a time instead of a 2 GiB blob
}File Handler
The server provides an optimized path for serving static files via the StaticHandler builder class, registered alongside regular processors:
use TrueAsync\StaticHandler;
$static = (new StaticHandler('/assets/', '/var/www/public'))
->setIndexFiles('index.html')
->enablePrecompressed('br', 'gzip', 'zstd')
->setCacheControl('public, max-age=86400');
$server->addStaticHandler($static);The first argument is the URL prefix, the second is the on‑disk directory. The handler aims to serve files as fast as possible without invoking PHP code.
MIME types – built‑in table for 44 extensions, with custom overrides via setMimeType().
Conditional requests – weak ETag based on (mtime, size, inode), handling If‑None‑Match / If‑Modified‑Since and returning 304, with HEAD support.
Range requests – supports 206 Partial Content, Content‑Range, and proper 416 for invalid ranges.
Pre‑compressed files – serves .br, .gz, .zst files when present and accepted by the client.
Security – prevents path traversal ("../", "%2e%2e", NUL, backslashes), disables dot‑files (e.g., ".git/"), offers configurable symlink policies (Reject/Follow/OwnerMatch), and hides files via glob patterns.
Open‑file cache – optional per‑processor cache with LRU and TTL, skipping stat, ETag calculation and MIME lookup; benchmarks show about +20 % performance on hot datasets. Useful for serving confidential files generated on‑the‑fly without involving PHP.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Open Source Tech Hub
Sharing cutting-edge internet technologies and practical AI resources.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
