I have spent the last while building php-quic, a PHP extension that exposes raw QUIC transport, both client and server, with first-class access to QUIC streams. The thing I am most happy about is what is not in it: no ngtcp2, no quiche, no Rust, no FFI. It is built directly on OpenSSL 3.5’s native QUIC stack, so the only dependency is an OpenSSL you very likely already have.
Why Bother
QUIC has been the transport under HTTP/3 for a few years now, and it is creeping into other protocols too: DNS-over-QUIC, SMB, and a growing list of custom things people are building when they want streams, TLS, and congestion control without standing up TCP plus a separate TLS layer. PHP had no real way to speak it. If you wanted QUIC you reached for a C library with its own TLS stack bolted on, wrapped it in FFI or a custom extension, and inherited a second copy of all the certificate and crypto logic you already trust OpenSSL to handle.
The reason this is suddenly worth doing is OpenSSL 3.5. It ships a native QUIC implementation, client and server, that reuses the same TLS engine everything else in your stack already uses. That removes the entire argument for pulling in a Rust or C QUIC library just to get a handshake. So php-quic is thin on purpose: it is a binding to a transport that is already there, not a reimplementation of QUIC.
What It Actually Does
It gives you the transport and gets out of the way. You get connections, you get streams, you get the event-loop primitive to multiplex them. What you do not get is protocol framing. HTTP/3, DNS-over-QUIC, or whatever you are building on top, that is yours to write. I made that split deliberately. The transport is the hard, fiddly, easy-to-get-wrong part, and the framing is the part that differs for every protocol.
The API is small. Three classes and a poll function:
Quic\Connection: a client connection, withopenStream(),acceptStream(),close(), and certificate and crypto inspection.Quic\Listener: the server side, bind a host and port andaccept()connections.Quic\Stream: the actual data path, withwrite(),read(),end(), andreset(). Bidirectional or unidirectional.Quic\poll(): the event-loop primitive for non-blocking, multiplexed I/O across many streams.
A client connection is about as short as you would hope:
$conn = new Quic\Connection('cloudflare-quic.com', 443, ['alpn' => 'h3']);
$control = $conn->openStream(false);
$control->write("\x00\x04\x00");
That opens a QUIC connection, negotiates the h3 ALPN, and opens a unidirectional control stream. Everything past that first byte is HTTP/3 framing that you write yourself.
A Concrete Example: DNS-over-QUIC
DNS-over-QUIC is a clean fit for QUIC: one query per bidirectional stream, a two-octet length prefix, done. With php-quic the transport part collapses to a few lines:
$conn = new Quic\Connection('dns.adguard-dns.com', 853, ['alpn' => 'doq']);
$stream = $conn->openStream();
$stream->write(pack('n', strlen($query)) . $query, true);
The true on the write sends the FIN with the data, which is exactly what DoQ wants: one query, stream half-closed, read the response back. That is the whole transport. If you want to see DNS-over-QUIC running against a real resolver without any of this, mrdns.com has the diagnostic tooling.
Non-Blocking by Default
QUIC multiplexes many streams over one connection, so a blocking read-one-thing-at-a-time model throws away the entire point. The poll() primitive lets you wait on a set of streams and act on whichever is ready:
Quic\poll([[$stream, Quic\POLL_READ | Quic\POLL_ERROR]], 1.0);
$chunk = $stream->read(65535);
That is enough to drive a real event loop, and on the server side a Quic\Listener can fan out across processes with SO_REUSEPORT so you scale horizontally without a load balancer in front doing connection-aware routing.
Requirements and Install
You need PHP 8.4 or newer (8.5 on Windows) and OpenSSL 3.5.0 or newer built with the native QUIC stack. The OpenSSL version is the real gate here, since 3.5 is recent. The easy path is PIE:
pie install mikepultz/php-quic
Or build it the usual way and add extension=quic to your php.ini:
phpize
./configure --with-quic
make && make test
sudo make install
What It Does Not Do Yet
I would rather be honest about the edges than oversell this. A few QUIC features are not in the first cut: no unreliable datagrams (RFC 9221), no 0-RTT early data, and no connection migration. There is also a small per-stream memory retention, on the order of 300 bytes, that matters only on very long-lived connections opening enormous numbers of streams. None of these block the protocols I built it for, but if your use case depends on datagrams or 0-RTT, know that going in.
Try It
The code is on GitHub under BSD-3-Clause. If you have wanted to speak QUIC from PHP, whether that is HTTP/3, DoQ, or something of your own, I would genuinely like to hear what you build with it and where the API gets in your way. Issues and pull requests welcome.