--- title: "PHP produces, Go consumes — over Redis" description: "A PHP producer and a Go worker share one Redis queue, exchanging the identical JSON envelope — no shared class, and no PHP runtime on the Go side." source: https://babelqueue.com/blog/php-produces-go-consumes-redis/ pubDate: 2026-06-25T00:00:00.000Z --- A PHP producer writes a job to a Redis list, and a Go worker reads the same bytes off the same list — no shared class, no PHP runtime on the Go side. This works because both ends speak one wire contract: the canonical BabelQueue envelope at `schema_version: 1`, routed by a URN instead of a class name. The code below is the [`redis-orders` example](https://github.com/babelqueue/babelqueue-examples), running against the published 1.0 SDKs. Start Redis (`docker run -d -p 6379:6379 redis:7`), run the PHP producer once, then run the Go worker. ## The producer: PHP, framework-less The PHP core ships an `EnvelopeCodec` and an optional `RedisTransport` (a thin wrapper over predis that does a plain `RPUSH`). A job is anything implementing `PolyglotJob` — a stable URN plus a pure-JSON payload: ```php use BabelQueue\Codec\EnvelopeCodec; use BabelQueue\Transport\RedisTransport; use Predis\Client; $queue = 'orders'; $transport = new RedisTransport(new Client('redis://localhost:6379/0'), $queue); $messages = [ new OrderMessage('urn:babel:orders:created', ['order_id' => 1042, 'amount' => 99.90, 'currency' => 'USD']), new OrderMessage('urn:babel:orders:created', ['order_id' => 1043, 'amount' => 12.50, 'currency' => 'EUR']), new OrderMessage('urn:babel:catalog:item.indexed', ['sku' => 'WIDGET-1', 'title' => 'Café Widget ☕']), ]; foreach ($messages as $message) { $envelope = EnvelopeCodec::fromJob($message, $queue); $transport->publish(EnvelopeCodec::encode($envelope), $queue); } ``` `EnvelopeCodec::fromJob($message, $queue)` builds the envelope; `encode(...)` turns it into the JSON string that lands on the queue. `RedisTransport::publish` takes that string and runs `RPUSH orders`. Nothing PHP-specific touches the wire — no `serialize()`, no class name, no object graph. Here is one of the encoded envelopes, exactly as it sits in Redis: ```json { "job": "urn:babel:catalog:item.indexed", "trace_id": "7b3f9c2a-e41d-4f88-9b2a-1c0d5e6f7a8b", "data": { "sku": "WIDGET-1", "title": "Café Widget ☕" }, "meta": { "id": "0f8a…", "queue": "orders", "lang": "php", "schema_version": 1, "created_at": 1750809600000 }, "attempts": 0 } ``` The identity is the URN in `job`. The payload is plain JSON in `data`. The only field that announces the producer is `meta.lang: "php"` — and the consumer treats that as information, not a dependency. ## The consumer: Go, no PHP in sight The Go SDK is a codec plus an optional `App` runtime. Broker drivers live in separate modules, so the core stays dependency-free; here we pull in the `/redis` transport, which uses the same reliable-queue pattern (`RPUSH` to produce, `BLMOVE` to reserve, `LREM` to ack): ```go import ( babelqueue "github.com/babelqueue/babelqueue-go" bqredis "github.com/babelqueue/babelqueue-go/redis" ) transport, _ := bqredis.New("redis://localhost:6379/0") defer transport.Close() app := babelqueue.NewApp(transport, babelqueue.WithDefaultQueue("orders")) app.Handle("urn:babel:orders:created", func(_ context.Context, env babelqueue.Envelope) error { fmt.Printf("[go] order created id=%v amount=%v %v trace=%s (produced by %q)\n", env.Data["order_id"], env.Data["amount"], env.Data["currency"], env.TraceID, env.Meta.Lang) return nil }) app.Handle("urn:babel:catalog:item.indexed", func(_ context.Context, env babelqueue.Envelope) error { fmt.Printf("[go] item indexed sku=%v title=%q (produced by %q)\n", env.Data["sku"], env.Data["title"], env.Meta.Lang) return nil }) processed, _ := app.Drain(ctx, "orders", 0) ``` `app.Handle(urn, handler)` registers a handler keyed by URN. `app.Drain(ctx, "orders", 0)` processes everything currently queued and returns the count — handy for a one-shot run; a long-lived worker calls `app.Consume(ctx)` instead, which blocks and routes by URN until the context is cancelled. The handler reads `env.Data` as a Go map, `env.TraceID` as a string, and `env.Meta.Lang` to see who produced the message. There is no generated stub, no shared schema package between the two services — just the envelope contract. ## What survived the language boundary Run the producer, then the consumer, and the Go side prints messages it never wrote: ``` [go] order created id=1042 amount=99.9 USD trace=7b3f9c2a-… (produced by "php") [go] order created id=1043 amount=12.5 EUR trace=… (produced by "php") [go] item indexed sku=WIDGET-1 title="Café Widget ☕" (produced by "php") [go] processed 3 message(s) — same envelope, different language. ``` Three things crossed intact, and they are the point: - **The `trace_id`** the producer generated arrives unchanged at the consumer. Preserving it across every hop is a contract invariant, so an end-to-end trace stitches together regardless of which language handled each step. - **Unicode** in `data` — `Café Widget ☕` — round-trips byte-for-byte. The envelope is UTF-8 JSON; no escaping surprises, no mojibake. - **The structured payload** stays structured. `order_id` is a number, `currency` is a string, and the Go map reads them as such — because `data` was JSON the whole way, never a PHP-serialized blob. (The `amount` field uses a JSON number here for a runnable demo; in production, represent money as minor units or a decimal string rather than a float.) ## Why this holds The two services never share a type. They agree on one document: the frozen wire envelope. PHP's `RedisTransport` and Go's `/redis` transport implement the same reliable-queue convention on a plain Redis list, so either language can sit on either end — swap the PHP producer for Go, Python, Node, Java, or .NET, and the Go worker keeps reading. All six SDKs are 1.0 GA and emit byte-compatible envelopes, verified by a shared conformance suite. That is the whole trick: stop serializing objects, start exchanging one JSON contract. The broker you already run carries it. Read the [wire contract](/docs/spec/1.x/envelope/) for the field-by-field definition, then wire up your own producer with the PHP [transports guide](/docs/php-sdk/1.x/transports/) and the Go [runtime and transports guide](/docs/babelqueue-go/1.x/runtime-and-transports/).