--- title: "Redis transport" description: "The BabelQueue.Redis transport — a canonical-envelope publisher and a URN-routed consumer over the §1 reliable-queue list pattern, built on StackExchange.Redis, on the framework-agnostic core." source: https://babelqueue.com/docs/babelqueue-dotnet/1.x/redis/ updated: 2026-06-14T00:00:00.000Z --- # Redis transport `BabelQueue.Redis` is a Redis transport on the .NET core, built on [StackExchange.Redis](https://www.nuget.org/packages/StackExchange.Redis). It sends the [canonical envelope](/docs/spec/1.x/envelope/) as a Redis list element with the [§1 reliable-queue pattern](/docs/spec/1.x/broker-bindings/#redis), and consumes by routing each message to a handler by URN — so a message it produces is consumed by any other BabelQueue SDK, and vice-versa. Unlike the SQS or RabbitMQ bindings, a Redis list element carries **no native metadata**, so there is **no header/attribute projection**: the list element **is** the canonical envelope JSON, byte-for-byte, with no wrapping and no added fields. A consumer in any language pops that same body and decodes it. ## Install ```bash dotnet add package BabelQueue.Redis ``` Requirements: **.NET 8**. It pulls `BabelQueue.Core` and `StackExchange.Redis` transitively. ## Produce ```csharp using BabelQueue.Redis; using StackExchange.Redis; var redis = await ConnectionMultiplexer.ConnectAsync("localhost:6379"); IDatabase db = redis.GetDatabase(); var id = await new RedisPublisher(db, "orders") .PublishAsync("urn:babel:orders:created", new Dictionary { ["order_id"] = 1042 }); ``` `PublishAsync(urn, data)` returns the message `meta.id`; pass a `traceId` to continue a trace. Produce is `RPUSH `. ## Consume ```csharp using BabelQueue; using BabelQueue.Redis; var handlers = new Dictionary { ["urn:babel:orders:created"] = async (env, rawBody, ct) => { // env.Data, env.TraceId, env.Attempts ... }, }; var consumer = new RedisConsumer(db, "orders", handlers, new RedisConsumerOptions { OnError = (err, env, raw) => Console.Error.WriteLine(err), }); await consumer.RunAsync(cancellationToken); // polls until cancelled ``` A reserve atomically moves the head of the queue onto a per-queue `:processing` list (`LMOVE :processing LEFT RIGHT`), so an in-flight message survives a crash; a successful handler `LREM`s it. Retry is **at-least-once**: a throwing handler leaves the element on the processing list. The poll loop never stops on a bad message — observe via `OnError` / `OnUnknownUrn`. Unknown-URN strategy is one of `fail` / `delete` / `release` / `dead_letter`. ## Contract mapping (§1) | Envelope | Redis | | :--- | :--- | | body | the list element (byte-identical across SDKs, no wrapping) | | `job` (URN) | read from the decoded body (no native metadata to route on) | | `trace_id` / `meta.id` / … | read from the decoded body | | produce | `RPUSH ` | | reserve | `LMOVE :processing LEFT RIGHT` (an in-flight message survives a crash) | | ack | `LREM :processing 1 ` | This is a **.NET-owned reliable queue**, mirroring the Go reference: produce/reserve/ack are self-consistent and crash-safe on a queue this SDK owns end-to-end. Full parity with Laravel's reserved-sorted-set reservation on a *shared* PHP+.NET Redis queue is a separate task — see broker-bindings §1.4. `IDatabase` is an interface, so the publisher and consumer unit-test against a mock with no Redis and no network. The envelope is unchanged (`schema_version` stays `1`); Redis support is purely additive.