spiral/roadrunner-tcp
RoadRunner TCP plugin provides PHP workers for RoadRunner's high-performance TCP server, letting you replace classic TCP setups with greater flexibility and speed. Install with Composer and pair with spiral/roadrunner for the app server.
Install the renamed package at ^4.3.1:
composer require roadrunner/tcp:^4.3.1
The package was renamed from spiral/roadrunner-tcp to roadrunner/tcp. Update your composer.json requirement and lockfile — the old spiral/roadrunner-tcp name is a different, frozen line. Also confirm the class namespace after the rename (Spiral\RoadRunner\TCP\... vs RoadRunner\TCP\...) by inspecting vendor/roadrunner/tcp/src; the composer name and the PHP namespace do not necessarily move in lockstep, so check before mass-renaming use statements.
Take 4.3.1, not 4.3.0: 4.3.1 fixes the package's Composer requirement, which previously pulled spiral/roadrunner (the v2-era bridge) instead of roadrunner/roadrunner. If you installed 4.3.0, you may have the wrong RoadRunner PHP bridge in your lockfile without any visible error:
composer update roadrunner/tcp --with-dependencies
composer show | grep -Ei 'roadrunner'
You want roadrunner/tcp and roadrunner/roadrunner; a leftover spiral/roadrunner entry is a v2 artifact — drop it if nothing else in your tree needs it.
Upgrade to RoadRunner v3: v4.3.0+ adds explicit RoadRunner v3 support and expects a v3 config. The v2 shape in the previous assessment no longer applies — the worker command moved under server, and the config declares a version:
version: '3'
server:
command: "php worker.php"
pool:
num_workers: 4
max_jobs: 500
ttl: 0
tcp:
listen: "tcp://127.0.0.1:6001"
Run rr serve -c .rr.yaml and confirm the TCP listener actually binds before debugging PHP — a silent plugin/config failure looks identical to a worker that never accepts.
First use case: a minimal echo/binary RPC worker. Instantiate the worker once outside the loop and reuse it for the whole process lifetime:
use Spiral\RoadRunner\TCP\Worker; // verify namespace on v4.3+
use Relay\Relay;
$worker = new Worker();
$relay = new Relay();
while ($relay->accept($worker)) {
$payload = $worker->read();
$worker->write('OK');
}
Request/response over TCP: read()/write() give you a byte stream, not messages. Define your framing contract up front (length-prefix, newline-delimited, TLV) and put encode/decode in a dedicated class so client and server cannot drift.
Binary payloads: For protobuf/msgpack/app structs, use a fixed header + payload or a 4-byte big-endian length prefix (pack('N', strlen($body))). Keep the framing layer separate from the codec so you can swap serializers without touching transport code.
Pool-per-purpose: Run dedicated TCP pools for distinct workloads (e.g. tcp-proxy, queue-bridge) and use max_jobs/ttl on the v3 pool config to recycle workers before memory drifts. Prefer max_jobs over a TTL for deterministic recycling.
Client-side usage: Use the package client from CLI tools and sibling services:
$client = new Spiral\RoadRunner\TCP\Client('127.0.0.1:6001');
$client->write($frame);
$response = $client->read();
Laravel integration: Do not boot the framework per message. Bootstrap Laravel once in worker.php, resolve a handler from the container inside the loop, and reset per-request state (auth user, DB transactions, queued listeners) between messages. Expose the worker as an artisan command for local testing (php artisan tcp:serve) that speaks the same framing as production. If you also run a Laravel/RoadRunner bridge that still declares spiral/roadrunner, expect both bridge packages to coexist after 4.3.1 — that's fine, but verify each resolves to a version targeting RR v3.
Upgrade pattern: Bump roadrunner/tcp, roadrunner/roadrunner, and the RoadRunner binary together in the same deploy. The v3 protocol/relay changes mean a v4.3 PHP worker against a v2 rr binary fails in confusing ways (handshake or relay errors, not a clear version message).
Dependency audit as a step: Add composer show roadrunner/tcp roadrunner/roadrunner to your upgrade checklist/CI output. The 4.3.1 fix was purely a dependency correction, so regressions in this area are invisible in application code and only show up as runtime handshake failures.
4.3.0 can silently install the wrong bridge: The bug fixed in 4.3.1 is a wrong Composer requirement, not a code bug — so a 4.3.0 install looks perfectly healthy while giving you v2-era RoadRunner classes. If you hit handshake/relay errors on a fresh 4.3.x deploy, check the installed bridge before touching .rr.yaml.
Package rename breaks CI caching: composer require spiral/roadrunner-tcp still resolves on old projects, so a stale lockfile or CI image can silently keep you on v4.2. Pin roadrunner/tcp:^4.3.1 and confirm with composer show roadrunner/tcp.
RoadRunner v3 is a hard requirement: v4.3.0+ is not a drop-in for v2 deployments. Audit .rr.yaml for the v3 schema (version, server.command, rpc.listen), and check any custom plugins or rr CLI flags in your Dockerfile/Makefile.
Framing is still manual: TCP has no message boundaries. A short read or a coalesced read will corrupt your protocol if you assume one read() equals one message. Always loop until your length prefix is satisfied, and test with nc sending partial frames.
Long-lived workers leak: Reset the Laravel container's request-scoped bindings each iteration, avoid unbounded static caches, and call gc_collect_cycles() periodically. max_jobs is your safety net, not your strategy.
Blocking I/O and timeouts: read()/write() can block for the whole worker slot. Set socket timeouts at connect time or wrap handlers so a single stuck client can't starve the pool.
No discovery or load balancing: Transport only. Put HAProxy/nginx-stream in front, or resolve backends from Redis/config in the client.
Extension points: Hook the RoadRunner worker lifecycle (v3 event classes) to flush buffers, close DB connections, and commit/rollback before the worker is recycled.
Debugging: bin2hex() your frames during development, and keep a plaintext echo handler behind a config flag so you can validate connectivity with nc independently of your binary codec.
How can I help you explore Laravel packages today?