# goridge: a 12 byte framing layer between PHP and Go over native sockets

> goridge is a codec library that lets PHP call Go service methods over TCP, Unix sockets or pipes, with a fixed 12 byte header, CRC32 verification and net/rpc compatibility, and a 4.0 release that moved it to Go modules at a new major version.

**roadrunner-server/goridge** — 🧙 High-performance PHP-to-Golang IPC/RPC bridge

- Repository: https://github.com/roadrunner-server/goridge
- Stars: 1,290 · Forks: 80
- Language: Go
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/roadrunner-server-goridge

## What the bridge is for

goridge is a high performance PHP-to-Golang codec library which works over native PHP sockets and the Golang net/rpc package. The point is to let you call Go service methods from PHP with a minimal footprint, with structures and []byte support.

The use case this serves is a PHP application that has one or more hot paths, an image processor, a PDF renderer, a search indexer, and you want that work done in Go while the rest of the application stays in PHP. The alternative is to rewrite the whole thing or to call a separate HTTP service, and the alternative to a separate HTTP service is a socket and a small codec, which is what this is.

The PHP side of the implementation lives in a separate repository, goridge-php, and the README links it from the top. This repository is the Go side, the codec and the framing.

The parent project is RoadRunner, described as a high-performance PHP application server, load-balancer and process manager written in Golang, and the README links it as the place to look for the wider system this codec belongs to. That context matters, because the codec is designed to be usable on its own, with a standalone protocol usage, and the ability to connect to existing applications, not only to RoadRunner-managed ones.

The project is MIT licensed with a LICENSE file at the root, the last push was on 2026-10-14, and the recent releases are v4.0.0 on 2026-10-08 after v4.0.0-beta.2 and v4.0.0-beta.1, so the 4.0 line has been in beta since March 2026.

## The wire format, and the 12 byte header

The feature list is specific about the protocol, and the details are what you would need to interoperate with another implementation.

The message footprint is 12 bytes over any binary payload, with binary error detection, and the header is verified with CRC32. So every frame carries a fixed 12 byte header and a payload, and the receiver checks the CRC before trusting the contents.

The transports are sockets over TCP or Unix, which requires ext-sockets, and standard pipes. There is also a note that unix sockets are powered, also on Windows, which is a portability point: a Go binary on Windows can serve over a Unix socket, which matters when the PHP side is in a container or in WSL.

Structured data transfer uses json, and []byte transfer is supported, including big payloads. The error handling is layered, with service, message and transport level errors, so a failure in the PHP business logic is distinguishable from a failure to read a frame.

The performance claim is 300k calls per second on a Ryzen 1700X over 20 threads. That is a number from the project, on a specific machine, and the frames are small in that measurement, so it is a figure about framing overhead rather than about payload throughput. The requirement of a 64bit PHP version is there because of the binary payload support and the pointer-sized fields.

The integration story is native net/rpc integration, so the Go side is an ordinary net/rpc server, and the codec plugs in as its codec. The repository has a pkg/ directory with the frame, pipe, rpc and socket packages, and an internal directory for the shared internals.

## The Go side, and how little of it there is

The sample in the README is a complete Go server in about thirty lines, and it shows the shape of the integration.

```go
package main

import (
	"fmt"
	"net"
	"net/rpc"

	goridgeRpc "github.com/roadrunner-server/goridge/v4/pkg/rpc"
)

type App struct{}

func (s *App) Hi(name string, r *string) error {
	*r = fmt.Sprintf("Hello, %s!", name)
	return nil
}

func main() {
	ln, err := net.Listen("tcp", ":6001")
	if err != nil {
		panic(err)
	}

	_ = rpc.Register(new(App))

	for {
		conn, err := ln.Accept()
		if err != nil {
			continue
		}
		_ = conn
		go rpc.ServeCodec(goridgeRpc.NewCodec(conn))
	}
}
```

The service is a struct with methods whose signatures follow the net/rpc convention, a request argument and a pointer to a response, returning an error. The method is registered with rpc.Register, and then each accepted connection is served with rpc.ServeCodec and a codec built from goridgeRpc.NewCodec.

So the amount of goridge-specific code on the Go side is a single call, and everything else is the standard library. That is the point of choosing net/rpc as the substrate rather than inventing a protocol, and it is why the ability to connect to existing applications works: if you already have a net/rpc service, the codec is the only piece you swap.

The Go module requirements are minimal. The direct dependencies are roadrunner-server/errors, stretchr/testify for the tests, and google.golang.org/protobuf, with go.yaml.in/yaml.v3 as an indirect dependency. The module targets Go 1.27 with the toolchain line at go1.27.2.

The install command, for the v4 line, is:

```go
GO111MODULE=on go get github.com/roadrunner-server/goridge/v4
```

The v4 in both the module path and the import path is not optional, since Go's semantic import versioning puts the major version in the path.

## What the v4 major bump changed for your import paths

The versioning of this library has a practical consequence that is easy to miss when you copy an example from a blog post.

The module path is github.com/roadrunner-server/goridge/v4, and the install command carries the same suffix. That is Go's semantic import versioning at work: once a module reaches v2 or above, the major version has to appear in the module path, and the import inside your code has to match it exactly. A project upgrading from the v3 line therefore has to change its imports, and the import alias in the sample, goridgeRpc, is a reminder that the package name and the path are not the same thing.

The release history shows this was not a quick bump. v4.0.0-beta.1 was published on 2026-03-08, v4.0.0-beta.2 on 2026-05-09, and v4.0.0 on 2026-10-08, so the 4.0 line spent about seven months in beta. The project is not archived and the last push was on 2026-10-14, six days after the stable tag, so the 4.0.0 release is recent and the branch is still moving.

The repository carries a .release-please-manifest.json and a release-please-config.json, which is the conventional way of doing a changelog and a version bump from commit messages, and a CHANGELOG.md alongside it. There is also a .golangci.yml for the linter and a codecov.yml for coverage, with the CI workflow split into three by operating system, Linux, macOS and Windows, which is consistent with the claim that it works on Windows and with the Unix-sockets-on-Windows note.

The directory layout follows the same split as the transports. There is a pkg/frame for the framing, pkg/pipe for the pipe transport, pkg/rpc for the net/rpc codec, pkg/socket for the socket transport, and an internal directory for the shared internals, plus a benchmarks directory and a tests directory. A separate tests/ directory alongside the package tests usually means end-to-end tests that exercise the bridge across processes rather than unit tests inside a package.

## The test matrix, and the fuzz targets

The Makefile is small and it says something about where the risk in this project sits.

The test target runs the suite several times over, and the pattern is deliberate. For each of the internal package, pkg/frame, pkg/pipe, pkg/rpc and pkg/socket, the tests are run twice: once with -race and coverage, and once without -race, both with the debug build tag. The double run is there because a race detector can change the timing enough to expose a different set of failures, and the debug tag exercises the debug-only code paths that the normal build compiles out. The pipe package is notably not run with -race, which is a reminder that a race detector on a synchronisation primitive tells you little.

The fuzz target is where the real interest is. There are five fuzz targets, each with a ten second budget:

```
go test -fuzz=FuzzReadFrame -fuzztime=10s ./pkg/frame
go test -fuzz=FuzzReadOptions -fuzztime=10s ./pkg/frame
go test -fuzz=FuzzVerifyCRC -fuzztime=10s ./pkg/frame
go test -fuzz=FuzzFrameRoundTrip -fuzztime=10s ./pkg/frame
go test -fuzz=FuzzReceiveFrame -fuzztime=10s ./internal
```

Four of the five target the frame package, which is the part that parses bytes off a socket, and one targets the receive path in the internals. The names describe the properties being checked: that a frame can be read without panicking, that the options can be read, that the CRC verification accepts and rejects the right things, that a frame survives a round trip through the codec unchanged, and that receiving a frame is safe.

That is the right place to spend effort in a bridge like this, because the PHP side is sending you bytes over a pipe and there is no type system on the other end of it. A malformed length field in a framing implementation is a memory safety bug or a hang, and fuzzing the reader is how you find those before someone else does.

## Conclusion

goridge fits a PHP team that wants to move hot paths into Go without rewriting the whole application, since the PHP side is a drop-in library and the Go side is a standard net/rpc server. It is a poor fit if your host cannot open a socket or a pipe to a sidecar process, and a poor fit if you need a protocol other than net/rpc, since the codec is tied to that package's framing. Before adopting, read the v4.0.0 release notes, because the module path changed to goridge/v4 and the install command changed with it, and check that your PHP is 64 bit and has ext-sockets if you plan to use TCP or Unix sockets.

## FAQ

### What is goridge?

It is a high performance PHP-to-Golang codec library that works over native PHP sockets and the Golang net/rpc package, letting you call Go service methods from PHP with a minimal footprint, with structures and []byte support. The PHP implementation lives in a separate repository, goridge-php.

### What transports does goridge support?

Sockets over TCP or Unix, which require ext-sockets, and standard pipes. Unix sockets are also powered on Windows, so a Go binary on Windows can serve over a Unix socket. There are no external dependencies or services, and a 64bit PHP version is required.

### How do I install goridge and use it with net/rpc?

Run GO111MODULE=on go get github.com/roadrunner-server/goridge/v4, then register your service with rpc.Register and serve each accepted connection with rpc.ServeCodec using goridgeRpc.NewCodec(conn). The v4 in the import path is required.

### How does goridge detect corrupted frames?

The framing adds a 12 byte header over any binary payload and the header is verified with CRC32, with binary error detection alongside it. The Makefile has dedicated fuzz targets for the frame reader, the options reader, CRC verification and a frame round trip.

## Sources

- [Issues](https://github.com/roadrunner-server/goridge/issues)
- [License: MIT](https://github.com/roadrunner-server/goridge/blob/master/LICENSE)
- [README](https://github.com/roadrunner-server/goridge/blob/master/README.md)
- [Releases](https://github.com/roadrunner-server/goridge/releases)
- [roadrunner-server/goridge on GitHub](https://github.com/roadrunner-server/goridge)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/roadrunner-server-goridge
