TrueAsync Server configuration
(PHP 8.6+, true_async_server 0.6+)
All server configuration is set through the TrueAsync\HttpServerConfig object before calling new HttpServer($config). Once HttpServer is constructed, the config is frozen: any setter on it throws HttpServerRuntimeException.
use TrueAsync\HttpServer;
use TrueAsync\HttpServerConfig;
use TrueAsync\LogSeverity;
$config = (new HttpServerConfig())
->addListener('0.0.0.0', 8080)
->addListener('0.0.0.0', 8443, tls: true)
->addHttp3Listener('0.0.0.0', 8443)
->setCertificate('/etc/tls/server.crt')
->setPrivateKey('/etc/tls/server.key')
->setWorkers(4)
->setKeepAliveTimeout(60)
->setMaxBodySize(50 * 1024 * 1024)
->setCompressionEnabled(true)
->setLogSeverity(LogSeverity::INFO)
->setLogStream(STDERR);
$server = new HttpServer($config);Setters return static, so the config is built as a chain.
Listeners
The server can listen on any number of TCP/Unix sockets and UDP ports (for HTTP/3) at the same time.
| Method | What it does |
|---|---|
addListener($host, $port, $tls = false) | TCP, HTTP/1.1 + HTTP/2 (h2c by preface on plaintext, h2 via ALPN on TLS) |
addHttp1Listener($host, $port, $tls = false) | TCP, HTTP/1.1 only. A client sending an HTTP/2 preface gets 400 |
addHttp2Listener($host, $port, $tls = false) | TCP, HTTP/2 only. Without TLS this is h2c and the preface is required |
addHttp3Listener($host, $port) | UDP, HTTP/3 / QUIC. TLS 1.3 is enabled automatically, the server certificate is used |
addUnixListener($path) | Unix socket, HTTP/1.1 + HTTP/2 (h2c-style) |
$config
->addListener('0.0.0.0', 80) // H1 + H2c
->addListener('0.0.0.0', 443, tls: true) // H1 + H2 over TLS
->addHttp3Listener('0.0.0.0', 443); // H3 / QUIC on the same portFor a phased HTTP/3 rollout you can temporarily disable the Alt-Svc advertisement:
$config->setHttp3AltSvcEnabled(false);TLS
$config
->setCertificate('/etc/tls/server.crt')
->setPrivateKey('/etc/tls/server.key');The certificate and key are shared across all TLS listeners (including HTTP/3). TLS 1.2/1.3, ALPN, weak ciphers disabled, stateless session tickets, safe renegotiation off.
Workers and bootloader
setWorkers(1) (the default) enables single-threaded mode: start() runs the event loop on the calling thread.
setWorkers(N > 1) spins up the built-in pool of N threads via Async\ThreadPool. Each worker re-binds the same listeners and the kernel (Linux/BSD) distributes accepts through SO_REUSEPORT. The parent start() waits for all workers to finish.
$config
->setWorkers(4)
->setBootloader(function () {
// runs once in each worker before the task loop
require __DIR__ . '/vendor/autoload.php';
Database::warmupPool();
OpcacheWarm::compile();
})
->setRequestScope(true); // default; false saves 2 allocs/req but nulls request_context()Hot reload
Replace the worker cohort without dropping a connection (pool mode only). Both triggers call HttpServer::reload(), which re-runs the bootloader in fresh workers on the same sockets:
$config
->enableHotReload([__DIR__ . '/app'], ['php'], debounceMs: 300, maxHoldMs: 2000) // watch files (dev)
->enableReloadOnSignal(); // SIGHUP (prod)Details: Multi-worker.
Timeouts
| Method | Default | What it times out |
|---|---|---|
setReadTimeout($sec) | — | receiving the full request |
setWriteTimeout($sec) | — | sending the response |
setKeepAliveTimeout($sec) | — | idle between requests; 0 disables keep-alive |
setShutdownTimeout($sec) | — | graceful shutdown: how long to wait for active requests |
Limits and backpressure
$config
->setBacklog(1024)
->setMaxConnections(50_000)
->setMaxInflightRequests(10_000)
->setMaxBodySize(10 * 1024 * 1024)
->setBackpressureTargetMs(10);setMaxConnections($n)— hard limit on the number of TCP connections.0removes the cap.setMaxInflightRequests($n)— admission control: once the in-flight handler count hits this limit, new requests get a fast rejection. H1 → 503 +Retry-After: 1; H2 →RST_STREAM REFUSED_STREAM(retry-safe per RFC 7540 §8.1.4). A hard connection cap does not help on H2, because new streams arrive over an already-accepted connection.0derives the limit asmax_connections × 10.setMaxBodySize($bytes)— maximum request body size. Default 10 MiB, range 1 KiB..16 GiB. H1 returns 413 and closes the connection; H2 sendsRST_STREAM(INTERNAL_ERROR).setBackpressureTargetMs($ms)— CoDel sojourn threshold for accept-side backpressure. When the per-request queue-wait stays above the threshold for 100 ms in a row, the listen socket is paused.0disables CoDel. Default 5 ms; 10–20 ms is typical for web workloads; 50–100 ms for slow handlers (database, IO).
Graceful drain (Step 8)
Controls for migrating load behind an L4 balancer:
| Method | Default | Purpose |
|---|---|---|
setMaxConnectionAgeMs($ms) | 0 (off) | After ±10% jitter on the limit, a connection gets Connection: close (H1) or GOAWAY (H2). Equivalent to gRPC MAX_CONNECTION_AGE. Production: 600_000 (10 min). |
setMaxConnectionAgeGraceMs($ms) | 0 | Hard-close after Connection: close/GOAWAY. 0 disables the force-close timer. |
setDrainSpreadMs($ms) | 5000 | Window for evenly spreading per-connection drain on CoDel trip / hard-cap (anti-thundering-herd). |
setDrainCooldownMs($ms) | 10_000 | Minimum gap between reactive drain triggers. |
HTTP/2 streaming limits
$config
->setStreamWriteBufferBytes(256 * 1024) // 256 KiB per stream, 4 KiB .. 64 MiB
->setH2StaticBudgetMax(0); // 0 = auto (memory_limit / 8)HttpResponse::send($chunk) blocks the handler coroutine only under backpressure, when the per-stream staging buffer is full. The default is 256 KiB (for comparison: gRPC-Go 64 KiB, Envoy 1 MiB, Node.js 16 KiB).
HTTP/3 production knobs
$config
->setHttp3IdleTimeoutMs(30_000) // RFC 9000 §10.1
->setHttp3StreamWindowBytes(256 * 1024) // per-stream flow control
->setHttp3MaxConcurrentStreams(100) // initial_max_streams_bidi
->setHttp3PeerConnectionBudget(16) // per-source-IP cap, slow-loris protection
->setHttp3SocketBufferBytes(8 << 20) // UDP rcv/snd buffer, absorbs inbound bursts
->setHttp3Pacing(false) // opt-in send pacing, for lossy/rate-limited paths
->setHttp3AltSvcEnabled(true); // RFC 7838 Alt-Svc advertisementThe connection-level initial_max_data is derived as window × max_concurrent_streams (the nginx pattern).
setHttp3SocketBufferBytes($bytes)— UDP socket receive/send buffer. Absorbs inbound bursts so they don't overflow intoRcvbufErrors. Default 8 MiB; the kernel clamps tonet.core.{r,w}mem_maxunless privileged.0leaves the OS default.setHttp3Pacing($bool)— cap each burst at the congestion controller'ssend_quantumand space packets on ngtcp2's pacing timer. Off by default: on a lossless path pacing only adds cost, so enable it for constrained paths only.
WebSocket
$config
->setWsMaxMessageSize(1024 * 1024) // 1 MiB, 128 .. 256 MiB
->setWsMaxFrameSize(1024 * 1024) // 1 MiB, same range
->setWsPingIntervalMs(30_000) // keepalive PING on idle
->setWsPongTimeoutMs(60_000) // deadline for the PONG reply
->setWsPermessageDeflate(false) // RFC 7692, off by default
->setWsMaxSubscriptions(0) // topic-filter cap per connection; 0 = no limit
->setWsPublishRateLimit(0); // publish() token bucket; 0 = offsetWsMaxMessageSize($bytes)— max size for a reassembled message. Going over it produces1009 Message Too Bigand closes the connection (RFC 6455 §7.4.1).setWsMaxFrameSize($bytes)— max size for a single frame. Guards against fragment-flood, where the client sends millions of tiny fragments.setWsPingIntervalMs($ms)— how often the server pings idle connections on its own.0disables the automatic ping.setWsPongTimeoutMs($ms)— how long to wait for PONG after a PING before treating the connection as dead and closing it with code1001 GoingAway.0disables the timeout.setWsPermessageDeflate($bool)— RFC 7692, message-level compression. Off by default: it's a deliberate opt-in, because compression costs CPU and widens the decompression-bomb attack surface. Negotiated only when the client itself offers this extension; requires a build with zlib.setWsMaxSubscriptions($count)— how many distinct topic filters one connection may hold.0(default) is no limit, as every self-hosted broker ships. Set it when client input reachessubscribe(); over the cap,subscribe()throwsWebSocketException.setWsPublishRateLimit($perSecond, $burst = 0)— per-connection token bucket overpublish(), the one WS call that causes work on every worker.0(default) is off. Over the rate,publish()throwsWebSocketBackpressureException.
See the WebSocket guide for the topic pub/sub model and the reference for the connection API itself.
Body streaming
Enables pull-based request-body streaming (issue #26): the H1/H2 parsers push chunks into a queue and the handler reads them through HttpRequest::readBody() without holding the entire body in RAM.
$config->setBodyStreamingEnabled(true);
$server->addHttpHandler(function ($req, $res) {
while (($chunk = $req->readBody()) !== null) {
// process chunk (e.g., stream write to disk, parse on the fly)
}
$res->setStatusCode(204);
});Without setBodyStreamingEnabled(true), the handler receives the already-read body through getBody(); readBody() is unavailable in that mode.
A 50-parallel-20-MiB-POST comparison (h2load, WSL2): peak RSS drops 1170 MiB → 197 MiB (×6), throughput climbs from 36 req/s → 100 req/s (×2.7), because handler dispatch no longer waits for the full body.
See also Streaming.
Auto-await body
$config->setAutoAwaitBody(true); // default: trueWhen enabled, non-multipart requests wait for the full body before calling the handler (multipart is always streamed). Useful for classic whole-body processing.
JSON
$config->setJsonEncodeFlags(JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);These flags are applied to HttpResponse::json() when the caller does not pass $flags explicitly. JSON_THROW_ON_ERROR is silently stripped: an encoding error produces a 500 with a JSON error body — the exception is not propagated into the handler.
Logging and statistics
For a single console stream, the setLogSeverity() / setLogStream() sugar is enough:
use TrueAsync\LogSeverity;
$config
->setLogSeverity(LogSeverity::INFO)
->setLogStream(STDERR); // any php_stream: file, php://stderr, php://memory, user wrapperThe logger is disabled by default (LogSeverity::OFF). Levels (OpenTelemetry SeverityNumber):
| Level | Contents |
|---|---|
OFF (0) | nothing |
DEBUG (5) | H3 packet tracing and similar |
INFO (9) | server lifecycle (start/stop), bind retries |
WARN (13) | TLS handshake fail, peer reset, absorbed exceptions |
ERROR (17) | listener bind failed, hard protocol errors |
FATAL is intentionally absent: it travels through zend_error_noreturn(E_ERROR), which already terminates the process.
Under a worker pool, do not use
setLogStream(). A parent-opened stream resource cannot cross into worker threads. UsesetLogSinks()with afile/stdout/stderrsink each worker can open itself.
For multiple destinations, a structured access log, syslog, or JSON output, use setLogSinks() — and opt into cross-worker getStats() with setStatsEnabled(true):
use TrueAsync\LogSeverity;
$config
->setStatsEnabled(true)
->setLogSinks([
['type' => 'file', 'path' => '/var/log/app/access.log',
'format' => 'json', 'category' => 'access', 'level' => LogSeverity::INFO],
['type' => 'stderr', 'format' => 'pretty', 'level' => LogSeverity::WARN],
]);Both are covered in full on the Observability page.
Telemetry (W3C Trace Context)
$config->setTelemetryEnabled(true);When enabled, incoming traceparent / tracestate are parsed and attached to the request. The following are available inside the handler:
$req->getTraceParent(); // raw header
$req->getTraceState();
$req->getTraceId(); // 32 lower-hex chars
$req->getSpanId(); // 16 lower-hex chars
$req->getTraceFlags(); // int (0x01 = sampled)Full reference
See TrueAsync\HttpServerConfig: all 60+ methods with detailed descriptions and valid value ranges.