Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Goridge Laravel Package

roadrunner/goridge

Goridge is the high-performance PHP-to-Go communication layer behind RoadRunner. It offers fast binary payload packing and RPC transport over sockets and pipes, letting PHP workers exchange data efficiently with the Go server. Lightweight and battle-tested.

View on GitHub
Deep Wiki
Context7

Getting Started

Goridge is the wire protocol RoadRunner (and any Goridge-speaking Go/PHP peer) uses to move binary frames over a socket or stdio. roadrunner/goridge is the PHP implementation: a Relay (transport) layer, a Frame (wire format) layer, and an RPC client on top. You rarely install it deliberately — it arrives transitively via roadrunner/roadrunner, roadrunner/roadrunner-worker, roadrunner/roadrunner-laravel, or Laravel Octane. Install it directly when you need to talk to a RoadRunner server from ordinary PHP, or when you're writing a custom worker.

Minimal setup

composer require roadrunner/goridge:^4.6

Two recent releases matter:

  • 4.5.0 (Oct 2026) — package renamed spiral/goridge → roadrunner/goridge. Namespace unchanged (Spiral\Goridge\...), so no use statements change. A package-name move, not a code move.
  • 4.6.0 (Oct 2026) — MsgpackCodec and Frame::CODEC_MSGPACK are deprecated. The codec still works, but you should stop writing new code against it (see Pattern 5 and the Gotchas). 4.6.0 also makes MsgpackCodec usable without ext-msgpack by falling back to rybakit/msgpack, and swaps the package's own dev dependency from spiral/roadrunner to roadrunner/roadrunner.

You need a Goridge peer listening. For RoadRunner, enable the RPC service in .rr.yaml:

rpc:
  listen: tcp://127.0.0.1:6001

Then confirm PHP can reach it:

php -r 'require "vendor/autoload.php";
$rpc = Spiral\Goridge\RPC\RPC::create("tcp://127.0.0.1:6001");
var_dump($rpc->call("app.Health", ""));'

Where to look first

  1. README.md — refreshed in 4.5.0 with the new package name, install snippet and badges; the Relay + RPC examples are still the whole API surface in two snippets.
  2. src/Relay/ — Relay::create() factory, SocketRelay (TCP/Unix), StreamRelay (any PHP stream, including STDIN/STDOUT).
  3. src/RPC/ — RPC::create(), codecs, and the call() signature.
  4. src/Frame.php / Payload — the 8-byte header (flags + length) and payload semantics; check the CODEC_MSGPACK deprecation annotation here.
  5. CHANGELOG.md — 4.6.0 is small (three entries) and worth reading in full before you touch codec config.
  6. The RoadRunner RPC service list (app.*, http.*, informer.*, jobs.*, metrics.*, kv.*, lock.*) — that's your playground.

First real use case: a health check you can schedule

// app/Console/Commands/RrHealth.php
public function handle(): int
{
    $rpc = RPC::create(config('roadrunner.rpc')); // tcp://127.0.0.1:6001

    try {
        $status = $rpc->call('app.Health', base_path());
        $this->info("RoadRunner status: {$status}"); // 200 = ok
        return self::SUCCESS;
    } catch (\Spiral\Goridge\Exceptions\RelayException $e) {
        $this->error("RoadRunner unreachable: {$e->getMessage()}");
        return self::FAILURE;
    }
}

That single call replaces a lot of "is the server up?" guesswork and is the cheapest way to prove your config, socket path, and protocol version all line up.

Implementation Patterns

Pattern 1 — RPC as a thin control plane

Treat the RPC client as infrastructure, not business logic. Bind it as a singleton so you don't reconnect on every call:

// app/Providers/RoadRunnerServiceProvider.php
$this->app->singleton(RPCInterface::class, fn () => RPC::create(
    config('roadrunner.rpc', 'tcp://127.0.0.1:6001')
));

Typical day-to-day control-plane calls:

Call Why you'd use it
app.Health readiness probe from a scheduler or /health endpoint
app.Pid / app.Version deploy verification, log enrichment
informer.Workers / informer.List debug stuck or leaking workers
http.Reset flush worker pool state after a config reload
metrics.* pull counters if you're not scraping the Prometheus endpoint

Pattern 2 — Never call RPC on the hot path

The relay is blocking, synchronous I/O with no built-in async. A 5 ms round trip inside a controller is 5 ms of a PHP-FPM/Octane worker doing nothing. Use RPC from: artisan commands, the scheduler, health endpoints, deploy scripts, and queue jobs — not from request handlers.

// routes/web.php — acceptable, because it's a dedicated probe route
Route::get('/health/roadrunner', function (RPCInterface $rpc) {
    return response()->json(['rr' => $rpc->call('app.Health', base_path())]);
});

Pattern 3 — Pick the right transport

Relay::create('tcp://127.0.0.1:6001');            // most common
Relay::create('unix:///var/run/rr.sock');          // lower overhead, no TCP stack
Relay::create('pipes:///path/to/socket');          // Windows / named pipes
new StreamRelay(STDIN, STDOUT);                    // worker side: protocol over stdio

Unix sockets are measurably faster and avoid exposing a TCP port. If you control .rr.yaml, prefer them in production.

Pattern 4 — Writing a custom worker

This is the case where you actually need goridge directly rather than the RoadRunner framework glue. The worker is a loop: read a frame, dispatch, write a frame back.

$relay = new StreamRelay(STDIN, STDOUT);

while ($payload = $relay->waitPayload()) {   // blocks until a frame arrives
    try {
        $result = handle($payload->body);
        $relay->send(new Payload((string) $result));
    } catch (\Throwable $e) {
        $relay->send(new Payload($e->getMessage(), flags: Payload::ERROR));
    }
}

Register it in .rr.yaml with command: "php worker.php" and the server will drive the loop for you, including graceful shutdown and max_jobs.

Pattern 5 — Codecs: JSON by default, msgpack on the way out

Default is JSON, which is fine, debuggable, and now the only built-in codec you should be planning around. As of 4.6.0, MsgpackCodec and Frame::CODEC_MSGPACK are deprecated:

// Still functional in 4.6.x, but deprecated — do not add new usages
$rpc = RPC::create($relay)->withCodec(new MsgpackCodec()); // requires ext-msgpack OR rybakit/msgpack

Two practical consequences of 4.6.0:

  1. It no longer hard-requires ext-msgpack. MsgpackCodec now initializes with rybakit/msgpack when the extension is absent, so it runs on stock PHP — noticeably slower, but no fatal Class "MessagePack" not found. Don't take that as a green light to deploy msgpack everywhere; the codec itself is on a deprecation path.
  2. Migration target is JSON or a custom codec. Replace ->withCodec(new MsgpackCodec()) with nothing (default JSON) unless you genuinely need compact binary framing, in which case implement CodecInterface yourself and own the format.

Both sides must agree on the codec. If you see garbage or "invalid header" errors, the first thing to check is a codec mismatch between your client and the server's RPC codec config.

For anything custom, implement the codec interface and decorate it:

final class LoggingCodec implements CodecInterface
{
    public function __construct(private CodecInterface $inner) {}

    public function encode(mixed $payload): string
    {
        Log::debug('goridge →', ['payload' => $payload]);
        return $this->inner->encode($payload);
    }

    public function decode(string $payload, mixed $options = null): mixed
    {
        return $this->inner->decode($payload, $options);
    }
}

This is the cleanest extension point for tracing — wrap, don't fork. It's also the cleanest exit ramp from MsgpackCodec: wrap a binary codec of your choosing behind CodecInterface and you're insulated from whatever the built-in codec story becomes.

Pattern 6 — Error handling that distinguishes failure modes

Two very different things throw:

try {
    $rpc->call('jobs.Push', $job);
} catch (TransportException $e) {
    // socket dead, server restarted, wrong port — infrastructure problem
    $this->reconnect();
} catch (RelayException $e) {
    // remote returned a frame with the ERROR flag set — the call itself failed
    report($e);
}

Transport errors are recoverable by rebuilding the relay; remote errors mean the request reached RoadRunner and was rejected. Don't retry the latter blindly.

Pattern 7 — Testing without a live server

StreamRelay works over any PHP stream pair, so you can unit-test protocol code with stream_socket_pair() or a php://memory/php://temp pair and a fake peer. For RPC consumers, bind a stub in the container:

$this->app->instance(RPCInterface::class, new class implements RPCInterface {
    public function call(string $method, mixed $payload = null, mixed $options = null): mixed
    {
        return $method === 'app.Health' ? 200 : null;
    }
});

Never let a test suite depend on a running rr serve. This also means codec changes are testable in isolation — assert the encoded bytes round-trip through a php://temp pair rather than trusting a live server.

Pattern 8 — Streaming large payloads

Goridge frames carry a stream flag; the relay will chunk large bodies rather than buffering one giant frame. This matters if you're pushing multi-megabyte payloads through jobs.* — it works, but memory still scales with the unserialized body on the PHP side. For big blobs, store an object and pass a key instead. Note that with msgpack deprecated, "shrink the payload with a binary codec" is less of an escape hatch than it was — a pointer/key is the durable answer.

Gotchas and Tips

4.6.0 deprecates msgpack — audit now, migrate on your own schedule. MsgpackCodec and Frame::CODEC_MSGPACK still work in 4.6.x; deprecation is a signal, not a removal. Grep for both before your next roadmap planning:

grep -rn "MsgpackCodec\|CODEC_MSGPACK" app/ config/ tests/
composer show | grep -i msgpack

If you find hits, confirm the server side too — a .rr.yaml RPC codec setting of msgpack will need to move in lockstep with the PHP client. Mixed codecs across a deploy window is the classic "invalid header" outage.

ext-msgpack is no longer a hard requirement — but the fallback has a cost. 4.6.0 initializes MsgpackCodec with rybakit/msgpack when the extension is missing, which turns a fatal error into a silent performance regression. If you're staying on msgpack short-term, keep ext-msgpack installed in production images and add ext-msgpack to composer.json's require (or a CI check) so you notice when a base image drops it. The absence of a crash is not evidence the fast path is being used.

4.5.0 is a rename, not a rewrite — and 4.6.0 finishes the job internally. 4.5.0 renamed the package spiral/goridge → roadrunner/goridge with no API, namespace, or wire-protocol changes. 4.6.0 then switched the package's own dev dependency from spiral/roadrunner to roadrunner/roadrunner, which means the maintainers' test matrix no longer exercises the legacy name at all. Practically: the legacy package name is now firmly second-class, and any remaining spiral/* pin in your dependency tree is a you-problem, not something upstream will catch.

Don't require both names. If spiral/goridge is still pulled in transitively — older spiral/roadrunner v2 lines do exactly this — you can end up with two packages declaring the same Spiral\Goridge\* classes, which is a fatal Cannot declare class Spiral\Goridge\Relay. Diagnose before upgrading:

composer why spiral/goridge
composer why roadrunner/goridge
composer show | grep -i goridge

If an old transitive dep pins the legacy name, either upgrade that dep or add a replace entry once you've confirmed nothing on the legacy package's file layout is load-bearing. In a monorepo with several Laravel apps, do the rename per-app and commit lock files together — a halfway state across a shared vendor/ is the painful case.

The v2/v3 API break is the #1 source of confusion. Nearly every blog post and Stack Overflow answer you'll find is v2:

// v2
$relay = new SocketRelay('127.0.0.1', 6001, SocketRelay::SOCK_TCP);
$rpc   = new RPC($relay);
// v3+
$rpc = RPC::create('tcp://127.0.0.1:6001');

Check composer show roadrunner/goridge before copying any snippet, and confirm it reports 4.6.x (or at least 4.5.x) before you trust the README you're reading. Snippets that still import from spiral/goridge aren't wrong code — the namespace is identical — but they're a sign the source predates the rename and may also predate the v3 factory API. Snippets using MsgpackCodec are now additionally a sign the source predates 4.6.0's deprecation.

The RPC port is unauthenticated. Anyone who can reach :6001 can reset your worker pool or push jobs. Bind to 127.0.0.1 or a Unix socket, and never publish it in a container/Compose port mapping. This is the single biggest operational footgun, and neither the rename nor the codec deprecation changes it.

Relays are stateful and not reconnect-safe. RoadRunner restarts, the socket dies, and the next call() throws TransportException on a stale relay. If you hold a long-lived RPC singleton (e.g., in Octane), catch transport errors and rebuild:

private function rpc(): RPCInterface
{
    try { $this->rpc->call('app.Health', ''); }
    catch (TransportException) { $this->rpc = RPC::create($this->dsn); }
    return $this->rpc;
}

Also: the relay is not fork-safe and not coroutine-safe. One relay per process; don't share across pcntl_fork or Swoole coroutines.

There are no default read timeouts. A hung peer blocks a PHP worker indefinitely. Set them on the underlying stream (or keep calls behind a supervisor) if you're calling from anything user-facing:

stream_set_timeout($stream, 2);

Msgpack rejects objects. The codec serializes plain data — arrays, scalars, null. Passing a Collection or a DTO will fail or silently lose type. Run ->toArray() (or json_decode(json_encode($dto), true)) first. JSON codec has the same rule but fails louder. If you were relying on msgpack's compactness to make a fat payload "fit", the deprecation is a good prompt to fix the payload shape rather than the codec.

app.Health returns an int, not a boolean. 200 is healthy; compare against 200, don't truthy-check. Also pass the project root as the payload — some health variants use it to validate the working directory.

Payload limits and frame corruption. If you log raw bytes during debugging, remember the first 8 bytes are the header — 4 bytes of flags/version and 4 bytes of length. A "random garbage" payload almost always means: wrong version, wrong codec, or you're reading a TCP stream that's split mid-frame. Use waitPayload(); don't read the stream yourself. With 4.6.0 in play, add "one side silently fell back to rybakit/msgpack while the other uses the extension" to that list — same format, different implementation, so it normally works, but it's the first thing to rule out if you're chasing an odd serialization difference between environments.

Debugging checklist, in order:

  1. rr serve -d -v — the Go side logs handshake and RPC errors loudly.
  2. tcpdump -i lo -X port 6001 or socat -v — confirm bytes are actually flowing both directions.
  3. Verify the rpc.listen config and that rr is actually running (not just "container up").
  4. composer show on both sides — check for a v2 client against a v3 server (silent hang), split package names across a build, and any lingering MsgpackCodec / CODEC_MSGPACK usage that should be migrating.

Don't pull this in for a queue driver. If you want RoadRunner Queues, roadrunner/roadrunner-queue (or the Laravel bridge) gives you a proper Illuminate\Queue driver. Reaching for jobs.* RPC by hand inside Laravel means you own retries, backoff, and failed-job handling yourself — the framework already does that.

Extension points worth knowing: custom RelayInterface (e.g., an in-memory relay for tests, or a retrying decorator), custom CodecInterface (logging, tracing, encryption — and now the recommended replacement for MsgpackCodec), and Payload flags for error/stream propagation. The library is thin enough that composition beats modification — it's ~1k lines and stays out of your way.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
ippanelcom/laravel-sdk
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus