Why PHP might become the best language for AI Agents

QuicPro Async - PHP extension

new open source project - not explicitely for agents and LLM but will work really well in that context - made with OpenAI models

I am currently working on a PHP PECL - an extension that will add some functionality to PHP - like true async, none-blocking IO, websocket Server and a server functionality itself.

Basically it will provide all the stuff that is currently missing in PHP to make AI run smoothly.
And since it is a PECL this will also work in PHP 8.1+ - So you won’t have to wait for PHP 9 or maybe even PHP 10 - PHP internals are currently discussing the RFC for true async - and you can create streaming and websocket server without the need for externals programs (e.g. mercure for symfony).

How? I am cheating. Kind of. I make a wrapper in C that wraps quiche (written in rust - by cloudflare - therefor we can assume it is a battle tested and solid QUIC implementation - QUIC is basically providing HTTP3 - runs over UDP instead of TCP and saves us a TLS handshake per request - which means we save 40-50ms per request and many many many more things up to new TLS3 encryption) .


I am using o3 and gpt4o to build it - and a thought technique that may be best described as “like a diffusion model”.

Which means I am building a rough structure, then build the pecl, then run it, add some stubbs then write some documentation, then build a test framework (for phpunit and fuzz tests on the C code) and go deeper and deeper until I reach a point where we have the full implementation of QUIC.

I have now reached a state where I just write the documentation. The whole approach is called documentation first development.

You can read what the library / application should be capable of - you can show it around and ask for feedback while working on the code itself.

I’ll go through it in detail later - but any help is truly welcome in case you want to learn how to prompt quality code with AI.


Current status

  1. Core QUIC handshake, stream send/receive, epoll scheduler ✓
  2. PHP stubs generated, unit + C fuzz tests scaffolded ✓
  3. Docs & examples drafted (health-check, Fibers, WebSocket chat) ✓
  4. Polishing error codes + shared ticket cache docs → now
  5. Next up: packaging & CI for ARM + x86, Composer helpers

I had made a first version to check if it basically can do that and run a benchmark over it without tls just to check what we could possibly get speed wise. And it was at some point even 10 times faster than Curl (curl also implements quiche - marked as experimental but doesn’t provide the websocket and server functionality).

Want to play?

git clone https://github.com/intelligent-intern/quicpro_async

Here is something from the documentation:

Async I/O and Parallel Execution with quicpro_async

A detailed guide explaining how non-blocking QUIC sockets, PHP Fibers and fork-per-core workers combine to deliver low-latency and full CPU utilisation.


1 · How the pieces fit

             +----------------------+                          +----------------------+
             |   Process 1          |                          |   Process 2          |
             |  (CPU core 0)        |                          |  (CPU core 1)        |
             |   epoll loop         |                          |   epoll loop         |
             |  Fiber A  Fiber B    |   shared UDP :4433 port  |  Fiber C  Fiber D    |
             +----------------------+                          +----------------------+
                    ↑                                                 ↑
                    |            shared TLS tickets (SHM-LRU)         |
                    └───────────── 0-RTT resumes across workers ──────┘
  • One process handles thousands of sockets concurrently with Fibers.
  • Several processes run in parallel on different CPU cores.
  • The shared-memory ticket ring lets any worker resume any client, so zero-RTT never breaks.

2 · Creating an async client

Rules: poll inside every receive loop, use short time-outs, close the session.

function fetch(string $path): string {
    $s  = quicpro_connect('example.org', 443);
    $id = quicpro_send_request($s, $path);
    while (!$r = quicpro_receive_response($s, $id)) {
        quicpro_poll($s, 5);       // yields ≤ 5 ms
    }
    quicpro_close($s);
    return $r['body'];
}

$jobs   = ['/a', '/b', '/c'];
$fibers = array_map(fn($p) => new Fiber(fn() => fetch($p)), $jobs);
foreach ($fibers as $f) $f->start();
foreach ($fibers as $f) echo $f->getReturn(), PHP_EOL;

3 · Single-process async server

use Quicpro\Config;
use Quicpro\Server;
use Quicpro\Session;
use Fiber;

$cfg = Config::new([
    'cert_file' => 'cert.pem',
    'key_file'  => 'cert.pem',
    'alpn'      => ['h3'],
]);

$srv = new Server('[::]', 4433, $cfg);

while ($sess = $srv->accept(100)) {
    (new Fiber(fn() => handle($sess)))->start();
}

function handle(Session $s): void {
    $req    = $s->receiveRequest();
    $path   = $req->headers()[':path'] ?? '/';
    $stream = $req->stream();
    $stream->respond(200, ['content-type' => 'text/plain']);
    $stream->sendBody("echo $path\n", fin: true);
    $s->close();
}

4 · Fork-per-core server (parallelism + Fibers)

use Quicpro\Config;
use Quicpro\Server;
use Fiber;

$workers = (int) trim(shell_exec('nproc')) ?: 4;
$cfg = Config::new([
    'cert_file'     => 'cert.pem',
    'key_file'      => 'cert.pem',
    'alpn'          => ['h3'],
    'session_cache' => true          // shared ticket ring
]);

for ($i = 0; $i < $workers; $i++) {
    if (pcntl_fork() === 0) { worker($i, 4433, $cfg); exit; }
}
pcntl_wait($status);

function worker(int $id, int $port, Config $cfg): void {
    $srv = new Server('0.0.0.0', $port, $cfg);
    echo "[worker $id] ready\n";
    while ($s = $srv->accept(100)) (new Fiber(fn() => handle($s, $id)))->start();
}

function handle($sess, int $wid): void {
    $req    = $sess->receiveRequest();
    $stream = $req->stream();
    $stream->respond(200, ['content-type' => 'text/plain']);
    $stream->sendBody("hello from $wid\n", fin: true);
    $sess->close();
}

5 · Error handling

A full catalogue of exceptions, warnings, error constants and the appropriate recovery strategy is maintained in ERROR_AND_EXCEPTION_REFERENCE.md.
Consult that file whenever a call throws or returns false; it contains ready-to-paste try / catch patterns and decoding of quicpro_get_last_error() values.


6 · Performance tuning knobs

Knob Default When to change
Environment QUICPRO_BUSY_POLL off spin microseconds before yielding (ultra-low latency)
Environment QUICPRO_XDP empty attach AF_XDP socket for zero-copy
Ini quicpro.shm_size 65536 raise for many unique clients per second
Config max_idle_ms 30000 lower for faster resource cleanup

Busy-poll example

putenv('QUICPRO_BUSY_POLL=50');
putenv('QUICPRO_XDP=ens6');

7 · Troubleshooting flow

  1. Use curl --http3 -v and check for “TLS session resumed”.
  2. If absent, run php -i | grep quicpro.shm_size to confirm cache size.
  3. Increase shm_size or export / import tickets per request in FPM.
  4. Enable QP_TRACE=1 and inspect perf or strace output.

8 · Summary

  • Fibers provide thousands of concurrent sessions inside one PHP process.
  • Forked workers distribute work to every CPU core; the shared ticket ring preserves 0-RTT across processes.
  • Network I/O is fully non-blocking in C; PHP code only orchestrates Fibers.
  • Error details and recovery recipes live in ERROR_AND_EXCEPTION_REFERENCE.md.
  • Main tuning levers: shm_size for ticket capacity and poll time-outs.

With these patterns a PHP application matches Go-level concurrency and saturates modern multi-core servers — without leaving PHP.

TLS Session Resumption & Multi-Worker Scaling

How to build a modern, fast, and scalable QUIC / HTTP-3 backend in PHP – step by step, with zero-RTT reconnects and no external load-balancer.

This document starts with the very first handshake, explains what a session ticket is, and then walks through five practical retention strategies – from single-process scripts to fork-per-core servers.
Every section includes copy-and-paste examples, plain-English background, and troubleshooting notes. Only basic PHP and cURL experience required.


0 · The problem we solve

0 .1 Why each fresh handshake steals time

A QUIC + TLS 1.3 handshake adds one extra round-trip before PHP sees the request. On a 40 ms mobile link that delay is 40 ms you could avoid for every reconnect.

0 .2 Session tickets in one sentence

The server ends the handshake with an encrypted resume-me-later blob.
The client stores that blob and sends it in the very first packet next time. Both sides can then start encrypted application data immediately – this is the famous zero-RTT.

0 .3 Why many PHP workers break tickets

With classic FPM the kernel pins each UDP flow to a single worker. A ticket issued by worker A is useless when the next packet lands on worker B.
The quicpro_async extension fixes this with a shared-memory LRU ring buffer: every worker can resume every client. Linux fans packets out automatically via SO_REUSEPORT – no proxy, no sticky sessions.


1 · Five practical ways to keep a session alive

Nr Mode name Best for … How it works in practice
1 Live object long-running CLI apps, ReactPHP loops keep the $sess resource in a global variable
2 Ticket cache classic FPM, short CLI jobs export ticket → store in APCu / Redis / file → import later
3 Warm container AWS Lambda, Google Cloud Run keep $sess in file scope; reused container = free zero-RTT
4 Shared-memory LRU fork()-based multi-worker on one host compiled-in shm cache; workers read the same ring
5 Stateless LB many hosts behind anycast / layer-4 LB client keeps ticket (mode 2); any edge can resume it

Quick rule of thumb
• one PHP process → live object • several FPM children → ticket cache • forked workers → shared-memory LRU


2 · Compiling and configuring the shared ticket cache

2 .1 Build

The task scheduler and shared-memory ticket ring are enabled by default. No special flags are required.

phpize
./configure
make -j$(nproc)
sudo make install

2 .2 php.ini switches (environment variables still work but are optional)

; /etc/php/8.3/mods-available/quicpro_async.ini
extension               = quicpro_async.so

; size of the ticket ring (bytes); 128 kB ≈ 120 tickets
quicpro.shm_size        = 131072

; custom shm_open() name when several independent servers run
quicpro.shm_path        = /quicpro_ring

; choose retention mode: 0=AUTO 1=LIVE 2=TICKET 3=WARM 4=SHM_LRU
quicpro.session_mode    = 0

2 .3 Session-mode constants available in PHP

QUICPRO_SESSION_AUTO        (0)
QUICPRO_SESSION_LIVE        (1)
QUICPRO_SESSION_TICKET      (2)
QUICPRO_SESSION_WARM        (3)
QUICPRO_SESSION_SHM_LRU     (4)

Override php.ini for a single server:

$cfg = Quicpro\Config::new([
    'session_mode' => QUICPRO_SESSION_TICKET,
    'alpn'         => ['h3'],
]);

AUTO chooses the smartest mode at runtime:

  • live object when the process never exits
  • ticket cache under FPM
  • warm when a FaaS variable is detected
  • shm-LRU after fork() when shm cache is available

3 · Hands-on: zero-RTT echo server with one worker per core

The script below forks N children (default = number of CPU cores), binds them all to UDP :4433, and resumes tickets across workers. Each worker processes sessions inside a Fiber so long polls never block new handshakes.

examples/ticket_resumption_multiworker.php

<?php

declare(strict_types=1);

use Quicpro\Config;
use Quicpro\Server;
use Quicpro\Session;
use Fiber;

/* choose worker count */
$workers = (int) ($argv[1] ?? trim(shell_exec('nproc'))) ?: 4;
$port    = 4433;

/* shared config */
$cfg = Config::new([
    'cert_file'     => __DIR__ . '/../certs/server.pem',
    'key_file'      => __DIR__ . '/../certs/server.key',
    'alpn'          => ['h3'],
    'session_cache' => true,        // shared tickets
]);

echo "Spawning {$workers} workers on UDP :{$port}\n";

/* master forks */
for ($i = 0; $i < $workers; $i++) {
    if (pcntl_fork() === 0) { worker($i, $port, $cfg); exit; }
}
pcntl_wait($status);

/* worker loop */
function worker(int $wid, int $port, Config $cfg): void
{
    $srv = new Server('0.0.0.0', $port, $cfg);   /* SO_REUSEPORT implicit */
    echo "[worker {$wid}] ready\n";

    while ($sess = $srv->accept(100)) {
        (new Fiber(fn() => handle($sess, $wid)))->start();
    }
}

function handle(Session $s, int $wid): void
{
    $req    = $s->receiveRequest();
    $path   = $req->headers()[':path'] ?? '/';
    $stream = $req->stream();
    $stream->respond(200, ['content-type' => 'text/plain']);
    $stream->sendBody("echo from worker {$wid}: {$path}\n", fin: true);
    $s->close();
}

3 .1 Run

sudo setcap cap_net_bind_service=+ep $(command -v php)
php examples/ticket_resumption_multiworker.php 4

3 .2 Observe zero-RTT across workers

curl --http3 -k https://localhost:4433/foo
curl --http3 -k https://localhost:4433/bar -v | grep session
# should print “TLS session resumed” and a worker ID that may differ

4 · Where each mode shines in real projects

  • API gateway with dozens of upstream sessions → live object
  • Classical FPM web app → ticket cache (APCu is enough)
  • Lambda function with 200 ms bursts → warm container
  • Chat backend on a 16-core box → shared-memory LRU
  • Anycast edge nodes worldwide → client ticket cache + stateless servers

You can mix modes; they work independently.


5 · Troubleshooting

A full catalogue of exceptions, warnings, error constants and recovery recipes is maintained in ERROR_AND_EXCEPTION_REFERENCE.md. Consult that file whenever a call throws or returns false.


6 · Cheat-sheet to pin on your monitor

# compile (ticket ring is built-in)
phpize
./configure
make -j$(nproc)
sudo make install

# php.ini quick setup
quicpro.shm_size     = 131072
quicpro.session_mode = 4        ; force shared-memory LRU

# run eight workers on UDP :4433
php examples/ticket_resumption_multiworker.php 8

Now you know what session tickets are, why they cut latency, and how to keep them alive in every PHP deployment scenario – from a single CLI script to an eight-core QUIC server that scales without a load-balancer. Enjoy your instant handshakes. :rocket:

Usage Examples

1 · Single GET request (synchronous)

Why?
Health-checks, one-shot CLI scripts, cron jobs, or any task where you
need exactly one HTTP/3 request and do not care about overlap or
latency hiding.

$sess   = quicpro_connect('example.org', 443);
$stream = quicpro_send_request($sess, '/');

while (!$resp = quicpro_receive_response($sess, $stream)) {
    quicpro_poll($sess, 20);                  // blocks ≤ 20 ms per loop
}

echo $resp['body'];
quicpro_close($sess);

2 · Parallel GETs with Fibers (concurrent client)

Why?
Scrape three pages in parallel during a CMS warm-up, pre-render a trio
of micro-frontends, or fetch several API endpoints concurrently in one
CLI process.

function fetch(string $path): string {
    $s  = quicpro_connect('example.org', 443);
    $id = quicpro_send_request($s, $path);
    while (!$r = quicpro_receive_response($s, $id)) quicpro_poll($s, 5);
    quicpro_close($s);
    return $r['body'];
}

$jobs   = ['/a', '/b', '/c'];
$fibers = array_map(fn($p) => new Fiber(fn() => fetch($p)), $jobs);
foreach ($fibers as $f) $f->start();
foreach ($fibers as $f) echo $f->getReturn(), PHP_EOL;

3 · Streaming POST upload (chunked body)

Why?
Send a multi-gigabyte file from disk to an object-storage gateway without
loading it entirely into memory. The body is streamed in 16 kB chunks,
and the loop never blocks longer than one system call.

$s   = quicpro_connect('uploader.local', 8443);
$sid = quicpro_send_request(
    $s, '/upload', headers: ['content-type' => 'application/octet-stream']
);

$file = fopen('dump.raw', 'rb');
while (!feof($file)) {
    quiche_h3_send_body_chunk($s, $sid, fread($file, 16 * 1024));
    quicpro_poll($s, 0);                      // non-blocking
}
quiche_h3_finish_body($s, $sid);

while (!$r = quicpro_receive_response($s, $sid)) quicpro_poll($s, 10);
print_r($r);

4 · WebSocket chat client (HTTP-3 CONNECT)

Why?
Build a thin chat front-end, subscribe to an event stream, or interact
with a push API where the server talks back at arbitrary times.

$sess = quicpro_connect('chat.example', 443);
$ws   = $sess->upgrade('/chat');              // RFC 9220 CONNECT → WS

fwrite($ws, "hello from PHP\n");
while (!feof($ws)) echo "Server: ", fgets($ws);

5 · Minimal WebSocket server with Fibers (≤ 5 channels)

Why?
Prototype a low-traffic WebSocket endpoint—e.g. a who-is-online status
panel—without deploying Nginx Unit or Go. The Fiber budget caps active
chats at five, so RAM usage stays predictable.

use Quicpro\Config;
use Quicpro\Server;
use Quicpro\Session;
use Fiber;

$cfg    = Config::new(['cert_file' => 'cert.pem', 'key_file' => 'key.pem']);
$server = new Server('::', 4433, $cfg);

$handle = function (Session $sess): void {
    static $open = 0;
    if ($open >= 5 || !$sess->isWebSocket('/chat')) { $sess->close(); return; }
    $ws = $sess->upgrade('/chat');
    $open++;
    while (!feof($ws)) fwrite($ws, strtoupper(fgets($ws)));
    fclose($ws); $sess->close(); $open--;
};

while ($c = $server->accept()) (new Fiber($handle))->start($c);

6 · Warm-start in FaaS / serverless containers

Why?
Cut cold-start pain on platforms like AWS Lambda or Google Cloud Run: the
global $GLOBAL_SESS survives between invocations so repeat calls skip
both TCP and TLS handshakes.

/* Global survives container reuse */
$GLOBAL_SESS = null;

function handler(array $event): string {
    global $GLOBAL_SESS;
    $GLOBAL_SESS ??= quicpro_connect('api.internal', 4433);

    $id = quicpro_send_request($GLOBAL_SESS, '/data?id=' . $event['id']);
    while (!$r = quicpro_receive_response($GLOBAL_SESS, $id)) {
        quicpro_poll($GLOBAL_SESS, 5);
    }
    return $r['body'];
}

7 · Collecting stats & QLOG traces

Why?
Debug congestion control, retransmissions, or handshake details. QLOG is
the official QUIC trace format; tools like qvis and Wireshark read it.

$s = quicpro_connect('example.org', 443);
quicpro_set_qlog_path($s, '/tmp/example.qlog');   // live trace
$id = quicpro_send_request($s, '/');
while (!quicpro_receive_response($s, $id)) quicpro_poll($s, 10);
print_r(quicpro_get_stats($s));
quicpro_close($s);

Visualise the file with qvis.dev or Wireshark.


8 · Zero-copy XDP path + busy-poll tuning

Why?
Push latency to microsecond levels on dedicated hardware—think
high-frequency trading or telemetry bursts—by avoiding both syscalls and
kernel queues.

putenv('QUICPRO_XDP=ens6');          // AF_XDP socket
putenv('QUICPRO_BUSY_POLL=50');      // spin 50 µs before Fiber yield

$s  = quicpro_connect('10.0.0.2', 4433);
$id = quicpro_send_request($s, '/ping');

while (!quicpro_receive_response($s, $id)) quicpro_poll($s, 0); // busy-poll

$lat = quicpro_get_stats($s)['rtt'];
printf("RTT: %.1f µs\n", $lat / 1000);
quicpro_close($s);
AI Agent Examples

1 · Streaming chat completion (Server-Sent Events)

Why?
Stream tokens from an LLM back-end (e.g. OpenAI, Ollama, vLLM) to the
browser without waiting for the full answer.

$sess   = quicpro_connect('llm.internal', 4433);
$body   = json_encode([
    'model'  => 'gpt-4o',
    'stream' => true,
    'messages' => [['role'=>'user','content'=>'Hello, who won the 2022 World Cup?']]
]);
$hdrs   = ['content-type'=>'application/json'];

$id = quicpro_send_request($sess, '/v1/chat/completions', $hdrs, $body);

while (!$r = quicpro_receive_response($sess, $id)) {
    quicpro_poll($sess, 10);                     // max 10 ms latency
}
foreach (explode("\n", $r['body']) as $line) {
    if (str_starts_with($line, 'data: ')) {
        echo substr($line, 6);                   // stream token to stdout
    }
}
quicpro_close($sess);

2 · Ten parallel prompts with Fibers

Why?
Bulk-score a list of product reviews, extract keywords, or translate many
sentences in a single PHP worker without blocking.

function ask(string $prompt): string {
    $s  = quicpro_connect('llm.internal', 4433);
    $id = quicpro_send_request($s, '/v1/completions',
        ['content-type'=>'application/json'],
        json_encode(['model'=>'gpt-4o','prompt'=>$prompt])
    );
    while (!$r = quicpro_receive_response($s, $id)) quicpro_poll($s, 5);
    quicpro_close($s);
    return json_decode($r['body'], true)['choices'][0]['text'];
}

$prompts = array_map(fn($i)=>"Summarise text $i", range(1,10));
$fibers  = array_map(fn($p)=>new Fiber(fn() => ask($p)), $prompts);
foreach ($fibers as $f) $f->start();
foreach ($fibers as $idx=>$f) echo "#$idx → ", $f->getReturn(), PHP_EOL;

3 · Bidirectional WebSocket to a local RAG pipeline

Why?
Keep a long-lived connection to a Retrieval-Augmented-Generation server
so each query reuses the same context cache and avoids TLS overhead.

$sess = quicpro_connect('rag.local', 8443);
$ws   = $sess->upgrade('/chat');          // RFC 9220 CONNECT → WS

fwrite($ws, json_encode(['ask'=>'What is E-MC²?'])."\n");

while (!feof($ws)) {
    $msg = fgets($ws);
    echo "Agent says: ", $msg;
    if (str_contains($msg, '[DONE]')) break;
}
fclose($ws);
$sess->close();

4 · Fork-per-core agent gateway (zero-RTT + Fibers)

Why?
Expose many AI models behind one UDP port; each worker process handles its
own epoll loop while the shared ticket ring preserves 0-RTT reconnects.

use Quicpro\Config;
use Quicpro\Server;
use Fiber;

$cores = (int) trim(shell_exec('nproc')) ?: 4;
$cfg   = Config::new([
    'cert_file'=>'cert.pem', 'key_file'=>'key.pem',
    'alpn'=>['h3'], 'session_cache'=>true
]);

for ($i = 0; $i < $cores; $i++) {
    if (pcntl_fork() === 0) { serve($i, 4433, $cfg); exit; }
}
pcntl_wait($s);

function serve(int $wid, int $port, Config $cfg): void {
    $srv = new Server('0.0.0.0', $port, $cfg);
    echo "[worker $wid] online\n";
    while ($s = $srv->accept(50)) (new Fiber(fn() => route($s)))->start();
}

function route($sess): void {
    $req  = $sess->receiveRequest();
    $path = $req->headers()[':path'] ?? '/';
    match ($path) {
        '/v1/chat/completions' => proxy_to('llm1.internal', $sess, $req),
        '/v1/embeddings'       => proxy_to('embed.internal', $sess, $req),
        default                => $sess->close()
    };
}

function proxy_to(string $backend, $sess, $req): void {
    /* left as exercise: send to backend, stream back to client */
    $sess->close();
}

5 · Invoking a Model Context Protocol (MCP) tool over HTTP/3

Why?
MCP is the new open standard (adopted by Anthropic and OpenAI) that lets
any LLM call external tools through one uniform interface.
When your PHP app can hit an MCP server you gain model-agnostic tool
invocation: Claude, GPT-4o, or an open-source model can all run the same
calculator, database, or vector-search function without custom glue.

Scenario

You run a remote MCP server that exposes a calculator tool via HTTP
Server-Sent Events (SSE). The goal is to ask the tool “sqrt(144)” and
stream the result back as soon as it is ready.

use Fiber;

/* 1. open one QUIC session to the MCP server */
$sess = quicpro_connect('mcp.example', 4433);

/* 2. MCP servers use SSE for streaming responses             */
/*    POST body = JSON with tool name + arguments             */
$body = json_encode([
    'tool'   => 'calculator',
    'input'  => ['expression' => 'sqrt(144)'],
    'stream' => true                   // ask for streaming
]);
$hdrs = [
    'content-type' => 'application/json',
    'accept'       => 'text/event-stream',
];

/* 3. send the request */
$stream = quicpro_send_request($sess, '/mcp/v1/call_tool', $hdrs, $body);

/* 4. read Server-Sent Events as they come in                 */
/*    every event line starts with  “data: {JSON…}”           */
while (true) {
    $resp = quicpro_receive_response($sess, $stream);
    if ($resp) break;                  // final chunk received
    quicpro_poll($sess, 5);            // yield ≤ 5 ms
}

/* 5. split the SSE payload into lines, show only “data: …”   */
foreach (explode("\n", $resp['body']) as $line) {
    if (str_starts_with($line, 'data: ')) {
        $event = json_decode(substr($line, 6), true);
        printf("Δ %s\n", $event['chunk'] ?? $event['result'] ?? '');
    }
}

quicpro_close($sess);

Output

Δ 1 Δ 2 Δ . Δ 0

The tool streamed four chunks: “1”, “2”, “.”, “0”. Concatenated they give
12.0 – the square root of 144.

Error handling cheat-sheet

Most failures map to the standard ERROR_AND_EXCEPTION_REFERENCE.md.

Situation Likely exception / error code Quick fix
Tool name typo QuicErrorUnknownFrame / 4xx JSON Check tool field spelling
MCP server unreachable QuicSocketException Verify DNS / firewall
Bad JSON in request QuicTransportException (HTTP 400) json_encode-safe and re-send
SSE disconnect mid-stream QuicStreamException Retry idempotent call or fall back

These five examples now cover single-shot requests, parallel Fibers,
stream uploads, WebSockets, fork-per-core gateways and the new MCP
tool standard – everything you need to wire PHP agents to modern LLM
stacks.

QuicPro Async – Error-Handling Reference

This document lists all runtime exceptions, warnings, PHP errors, and error-code constants exposed by the quicpro_async extension.


1 · Exception Classes

Class name Thrown when …
QuicConnectionException a QUIC connection cannot be created or maintained
QuicStreamException stream creation, read, write, or shutdown fails
QuicTlsHandshakeException TLS 1.3 handshake fails (invalid certificate, timeout, etc.)
QuicTimeoutException any operation exceeds its deadline
QuicTransportException QUIC protocol violation (e.g. invalid frame, version mismatch)
QuicServerException server startup or runtime failure (port already in use, bind error)
QuicClientException client-side connection or response failure
QuicSocketException generic network/socket issue (unreachable, ECONNREFUSED, etc.)

Usage example

try {
    $sess = quicpro_connect('bad.host', 443);
} catch (QuicConnectionException $e) {
    error_log("connect failed: " . $e->getMessage());
}

2 · Runtime Warnings

Warning constant Situation
QuicWarningInvalidParameters invalid options provided to a function / constructor
QuicWarningConnectionSlow RTT or handshake takes unusually long; timeout imminent
QuicWarningDeprecatedFeature caller uses a soon-to-be-removed API flag
QuicWarningPartialData a datagram / stream frame was only partially processed

Warnings are emitted with trigger_error(..., E_WARNING) and do not throw.


3 · PHP Error Messages

Severity Example message
E_WARNING Connection handshake took too long
E_WARNING Received unknown QUIC frame type 0x1f
E_ERROR Unable to initialize QUIC context
E_ERROR TLS handshake failed: invalid certificate

E_ERROR terminates the request; E_WARNING can be silenced or converted to an exception via ErrorException.


4 · Error-Code Constants

Constant Value Meaning
QUIC_ERROR_OK 0 success
QUIC_ERROR_TIMEOUT 1 operation timed out
QUIC_ERROR_INVALID_STATE 2 function called in wrong session/stream state
QUIC_ERROR_CONNECTION_LOST 3 connection aborted unexpectedly
QUIC_ERROR_HANDSHAKE_FAILED 4 TLS handshake error
QUIC_ERROR_STREAM_ERROR 5 unrecoverable stream failure
QUIC_ERROR_TRANSPORT_ERROR 6 QUIC transport-level protocol error
QUIC_ERROR_INTERNAL 7 internal failure (e.g. OOM)
QUIC_ERROR_UNSUPPORTED_VERSION 8 QUIC version not supported by peer

Return-style APIs (e.g. quicpro_poll()) expose these codes via quicpro_get_last_error().


5 · Quick Reference Cheat-Sheet

  • Always catch the eight exception classes for robust error handling.
  • Monitor warnings in logs to spot slow joins, bad parameters, and deprecated flags.
  • Treat E_ERROR as fatal; design graceful degradation around E_WARNING.
  • Map numeric codes from quicpro_get_last_error() to the table above.
$sess = quicpro_connect('api.internal', 443);

$id   = quicpro_send_request($sess, '/data');
while (!quicpro_receive_response($sess, $id)) {
    if (!quicpro_poll($sess, 10)) {
        throw new RuntimeException('poll failed: ' . quicpro_get_last_error());
    }
}

Using the documentation first approach I can basically write the unittests based on that - e.g. having one per desired Exception or Error, then add benchmark tests (will use pulumi to run a couple servers and then start a couple hundret thousand agents in parallel and we’ll see what happens…

When all the tests are written I can implement the C code - or let’s say go deeper and fix what is wrong at the moment. The whole project is way to complex to give it to a model and tell it to make an outline and implement it. It takes context to make good code.

And deep research also is far away from getting that.

But I am sure we can build an agentic network that runs extremly fast based on this lib to change that :wink:

5 Likes

Why a C-level PECL like QuicPro Async outruns Go, Node.js and Python for raw QUIC/HTTP-3 throughput


:rocket: Against Go (net/http, ServeHTTP, built-in event loop)

C-extension edge What Go has to do instead
Direct sendmmsg/recvmmsg syscalls Wrap in the runtime’s poller, copy into Go slice
Zero GC pauses Stop-the-world GC every few ms
Lock-free ring buffers Channel / mutex for every packet or write
AF XDP & busy-poll flags are one setsockopt Needs CGO stub, breaks static build
Hand-rolled SIMD (AES-NI, AVX2) Separate .s files or pure Go fall-back
Hot-reload shared object Full binary restart, drops QUIC flows

Result: Go is fantastic for back-office APIs, but at 1 M pkts/s or
<100 µs tail latency
the C path wins.


:high_voltage: Against Node.js (libuv + V8)

C-extension edge What Node must do
One memory copy kernel → user Buffer object plus JS object
No GC, no hidden prototype chains V8 major/minor GC spikes (5–10 ms)
recvmmsg batching Not exposed in libuv
QUIC implemented inside the PECL Native addon → JS bridge → user code
AF XDP / SO_BUSY_POLL usable Mostly unavailable, or copy back to JS
Zero-copy, zero-TLS handshake Extra layer for each message

Great for prototyping and full-stack JS, but not for micro-second
edge-services.


:snake: Against Python (asyncio / Trio)

  • GIL means one interpreter thread – true parallelism requires
    multiprocessing (copy + pickle).
  • asyncio sits on the same epoll, plus a Python-level scheduler.
  • Each packet becomes a bytes object → ref-count → GC → latency spikes.
  • High-perf socket flags only via CFFI/Cython – at that point you’re back
    to writing C.

Python stays unbeatable for data science & glue, yet not for sustained
wire-speed networking.


Bottom line

QuicPro Async lives one syscall away from the kernel. (Load quiche (or s2n-quic, ngtcp2, quinn, …) as a native module and you’re at the same “one syscall away from the kernel” spo )

No GC, no double buffering, no user-land event loop – which is why it can
match or exceed Go/Node/Python in raw QUIC throughput while you write
ordinary PHP with Fibers (but we are talling about nano seconds - let’s keep it real - it is made for millions of agent calls - your standard openai wrapper app won’t have a huge advantage).

Yes, I admit it is cheating - but hey who cares.

2 Likes

Why PHP is a terrible language for AI agents

The encoding your code was written in: interpreted as the encoding of strings-as-bytes.

Can your PHP observe what it is served within?

**Content-Type: text/plain; charset="Windows-1252"**

**<meta http-equiv="Content-Type" content="text/html; charset=utf-16">**

This nature of the string type explains why there is no separate “byte” type in PHP – strings take this role. Functions that return no textual data – for instance, arbitrary data read from a network socket – will still return strings.

Given that PHP does not dictate a specific encoding for strings, one might wonder how string literals are encoded. For instance, is the string "á" equivalent to "\xE1" (ISO-8859-1), "\xC3\xA1" (UTF-8, C form), "\x61\xCC\x81" (UTF-8, D form) or any other possible representation? The answer is that string will be encoded in whatever fashion it is encoded in the script file. Thus, if the script is written in ISO-8859-1, the string will be encoded in ISO-8859-1 and so on. However, this does not apply if Zend Multibyte is enabled; in that case, the script may be written in an arbitrary encoding (which is explicitly declared or is detected) and then converted to a certain internal encoding, which is then the encoding that will be used for the string literals. Note that there are some constraints on the encoding of the script (or on the internal encoding, should Zend Multibyte be enabled) – this almost always means that this encoding should be a compatible superset of ASCII, such as UTF-8 or ISO-8859-1. Note, however, that state-dependent encodings where the same byte values can be used in initial and non-initial shift states may be problematic.

2 Likes

Hmm do you have any realworld example that would break it?

You know what I’ll do some experiments to detect the encoding in the quic layer.
And at least throw a notice.

Normally you as a dev take care of having same html encoding and server headers. but yeah many guys do that wrong.

and there is mb_detect_encoding

1 Like

Personally running legal doc analysis core on Slim4 + OpenSwoole. The first “break apart” of the platform was because node client got too many requests back (async job response sent back from the core) and could not keep up with PHP 8.2…

2 Likes

Here I chatted up an AI for you, simply to allow you to rethink the hypothesis of this topic’s title.

Like I wrote before mb_check_encoding.
It is a function that can be used to check that.

I love it when J and jochenschultz get into it :laughing:

You guys are both brilliant though and great conversation really interesting stuff.

3 Likes

Hey Jochen,

Because Fibers all run inside a single PHP process and thread, am I right that I don’t need locks when each Fiber writes to its own slice of a shared array (e.g. $A['Agents'][$i])? And, on Windows, would using parallel threads plus proc_open() still be the recommended way to get true concurrency alongside Fibers?

Can’t tell yet. Will do some experimenting and stress testing. There might be edge cases or maybe I decide something like sharedobjects or not. Not quiet sure yet.
Benchmarks will tell.

Honestly I have no idea. I didn’t touch Windows in years. But I guess I’ll find out.
I got to dig into it, install Windows - hmm might even exist in my bootloader but it’s been a long time that I saw that either.. rebooting a computer :sweat_smile: - and then we check for breaking race conditions and stuff.

1 Like

I have a feeling this could be used to orchestrate model training - decentralized and open up many new opportunities.
Maybe providing “easy to use” is not the number one priority.
More like getting the most out of the hardware.

1 Like

My Ryzen Threadripper 1950X gets bricked by Windows 11 in 5 months…

Time to learn Linux I guess ^^

1 Like

Yeah thank you. You’ve been the last guy using PHP on Windows afaik so I don’t need to build the dll then :sweat_smile:

1 Like