{
  "id": "go",
  "title": "Redis pub/sub with go-redis",
  "url": "https://redis.io/docs/latest/develop/use-cases/pub-sub/go/",
  "summary": "Implement Redis pub/sub messaging in Go with go-redis",
  "tags": [
    "docs",
    "develop",
    "stack",
    "oss",
    "rs",
    "rc"
  ],
  "last_updated": "2026-05-14T08:58:05-05:00",
  "children": [],
  "page_type": "content",
  "content_hash": "95b9da135226829e18eb32c8551619ce6a9bc4113799737869887cd80328ff67",
  "sections": [
    {
      "id": "overview",
      "title": "Overview",
      "role": "overview",
      "text": "This guide shows you how to implement a Redis-backed pub/sub broadcaster in Go with [`go-redis`](https://redis.io/docs/latest/develop/clients/go). It includes a small local web server built with the Go standard library so you can publish messages to named channels, add and remove subscribers live, and watch Redis fan out each message to every interested listener."
    },
    {
      "id": "overview",
      "title": "Overview",
      "role": "overview",
      "text": "Pub/sub lets your application broadcast events — chat messages, cache invalidation signals, presence updates, notifications — to many consumers without per-pair wiring. The publisher names a *channel*; every client currently subscribed to that channel receives the message, in publish order, with sub-millisecond fan-out.\n\nThat gives you:\n\n* Many-to-many event delivery with no message storage cost in Redis\n* Exact-match subscriptions (`SUBSCRIBE orders:new`) for known topics\n* Pattern subscriptions (`PSUBSCRIBE notifications:*`) for whole topic hierarchies\n* Live server-side introspection through `PUBSUB CHANNELS`, `PUBSUB NUMSUB`, and `PUBSUB NUMPAT`\n* At-most-once delivery: subscribers that are offline when a message is published miss it, so durable state should live in keys or a Stream, not in the pub/sub channel itself\n\nIn this example, the publisher side calls `PUBLISH` with a JSON-encoded body and counts how many subscribers Redis reported delivering to. Each in-process subscriber owns its own Redis connection plus a background goroutine that reads from the go-redis message channel and pushes each delivery into a per-subscriber ring buffer."
    },
    {
      "id": "how-it-works",
      "title": "How it works",
      "role": "content",
      "text": "The flow looks like this:\n\n1. The application calls `hub.Subscribe(ctx, name, channels)` or `hub.PSubscribe(ctx, name, patterns)`\n2. The helper opens a `*redis.PubSub` (one Redis connection per subscriber), reads its `Channel()` in a goroutine, and registers the new `Subscription` under its name\n3. The application (or another process) calls `hub.Publish(ctx, channel, message)`\n4. Redis fans the message out over every subscribing client's open socket\n5. Each subscriber's goroutine wraps the raw `*redis.Message` as a `ReceivedMessage`, appends it to a bounded per-subscriber buffer, and bumps the atomic counter\n6. The publisher receives the integer subscriber count back from `PUBLISH`, which is the number of clients Redis delivered to right then\n\nPattern subscriptions match channels by glob (`*`, `?`, `[abc]`). A single message that matches both an exact subscription and a pattern subscription is delivered twice — once as a `message` and once as a `pmessage`. The helper exposes which pattern matched (if any) through `ReceivedMessage.Pattern`."
    },
    {
      "id": "the-pub-sub-hub-helper",
      "title": "The pub/sub hub helper",
      "role": "content",
      "text": "The `RedisPubSubHub` type wraps the publish, subscribe, and introspection operations\n([source](https://github.com/redis/docs/blob/main/content/develop/use-cases/pub-sub/go/pubsub_hub.go)):\n\n[code example]"
    },
    {
      "id": "data-model",
      "title": "Data model",
      "role": "content",
      "text": "Pub/sub has no Redis keyspace footprint of its own — channels are server-side routing entries, not stored values. The hub keeps its own bookkeeping in process memory:\n\n[code example]\n\nThe implementation uses:\n\n* [`PUBLISH`](https://redis.io/docs/latest/commands/publish) to fan a JSON-encoded message out to every subscriber of a channel\n* [`SUBSCRIBE`](https://redis.io/docs/latest/commands/subscribe) for exact-match subscribers\n* [`PSUBSCRIBE`](https://redis.io/docs/latest/commands/psubscribe) for glob-style pattern subscribers\n* [`PUBSUB CHANNELS`](https://redis.io/docs/latest/commands/pubsub-channels) to list the channels with at least one active exact-match subscriber\n* [`PUBSUB NUMSUB`](https://redis.io/docs/latest/commands/pubsub-numsub) to count subscribers per channel\n* [`PUBSUB NUMPAT`](https://redis.io/docs/latest/commands/pubsub-numpat) to count active pattern subscriptions server-wide\n* The go-redis `*redis.PubSub.Channel()` helper, which spins up an internal goroutine that pumps messages off the socket into a Go channel — so the helper just ranges over that channel and never touches `Receive()` or `ReceiveMessage()` directly"
    },
    {
      "id": "publishing-messages",
      "title": "Publishing messages",
      "role": "content",
      "text": "`Publish()` JSON-encodes the message body, calls `PUBLISH`, and updates the per-channel publish counter:\n\n[code example]\n\nThe integer returned by `PUBLISH` is what Redis itself reports — the number of subscribers (direct and pattern) that received the message in that call. It's a useful sanity check that the channel name is actually being listened to: a steady stream of `0`s means you have a typo somewhere or your subscriber crashed."
    },
    {
      "id": "subscribing-to-channels",
      "title": "Subscribing to channels",
      "role": "content",
      "text": "`Subscribe()` creates a named in-process `Subscription` that owns its own `*redis.PubSub` and dispatch goroutine:\n\n[code example]\n\nInside `register`, the helper opens a `PubSub`, reads its `Channel()` to get a buffered Go channel of incoming messages, and starts a goroutine that pumps each delivery into the subscription's ring buffer:\n\n[code example]\n\nA few details matter here:\n\n* Each `Subscription` gets its own `*redis.PubSub` (and therefore its own connection from the client pool). Sharing one across business subscribers would couple their lifetimes — closing one would close the channel for the others.\n* `ps.Channel()` is the right API to use here: go-redis runs an internal goroutine that pumps off the socket into a buffered Go channel and reconnects on transient failures. The helper just ranges over that channel.\n* Closing the `PubSub` (via `sub.Close()`) closes its underlying message channel, which causes the `range` loop in `sub.run()` to terminate cleanly. The helper waits on a `done` channel to be sure the goroutine has finished before returning from `Close()`.\n\nThe dispatch goroutine wraps each `*redis.Message` as a `ReceivedMessage` and tries to decode the payload as JSON, falling back to the raw string if it doesn't parse:\n\n[code example]"
    },
    {
      "id": "pattern-subscriptions-with-psubscribe",
      "title": "Pattern subscriptions with PSUBSCRIBE",
      "role": "content",
      "text": "`PSubscribe()` works the same way but routes messages through `PSUBSCRIBE` so each binding is a glob, not a literal channel name:\n\n[code example]\n\nWhen a published channel matches a pattern, go-redis populates both `Message.Channel` (the actual channel) and `Message.Pattern` (the pattern that matched). Exact-match deliveries leave `Pattern` as the empty string, so the helper carries the pattern as a `*string` and serialises it as `null` in JSON for exact matches:\n\n[code example]\n\nThat distinction is useful for routing: a pattern subscriber can do one thing for the whole hierarchy (e.g., increment a counter) and dispatch on the specific channel within its handler (e.g., \"invalidate this region's cache\")."
    },
    {
      "id": "inspecting-active-subscribers",
      "title": "Inspecting active subscribers",
      "role": "content",
      "text": "Redis exposes a small set of pub/sub introspection commands that report on subscriber state without traversing any keyspace:\n\n[code example]\n\n`PUBSUB CHANNELS` only reports channels with at least one exact-match subscriber — pattern subscribers do not appear here. That's a deliberate Redis design choice: a glob like `*` would otherwise show up as a subscriber to every conceivable channel. `PUBSUB NUMPAT` covers the pattern side as a single global count."
    },
    {
      "id": "stats-and-history",
      "title": "Stats and history",
      "role": "content",
      "text": "`Stats()` reports publish and receive counters plus the size of the subscription registry:\n\n[code example]\n\n`DeliveredTotal` is what Redis itself counted; `ReceivedTotal` is what this process's in-memory subscribers saw. In a single-process demo they should track each other closely — a sustained divergence usually means a subscriber goroutine exited, or a subscriber crashed while a publisher kept publishing. (Pub/sub is at-most-once: if your subscriber wasn't connected at publish time, the message is gone.)\n\nThe `Stats` struct uses `json:\"...\"` tags with snake_case names so the demo UI's shared JavaScript can read the same wire shape across every client port."
    },
    {
      "id": "prerequisites",
      "title": "Prerequisites",
      "role": "content",
      "text": "* Redis 6.2 or later running locally on the default port (6379). Earlier versions still work for plain `PUBLISH`/`SUBSCRIBE`; `PUBSUB NUMPAT` is older than that.\n* Go 1.23 or later (matching the version declared in this port's `go.mod`).\n* The `go-redis` client. The included `go.mod` pins:\n\n  [code example]"
    },
    {
      "id": "running-the-demo",
      "title": "Running the demo",
      "role": "content",
      "text": ""
    },
    {
      "id": "get-the-source-files",
      "title": "Get the source files",
      "role": "content",
      "text": "The demo consists of three Go files plus a `go.mod` and `go.sum`. Download them from the [`go` source folder](https://github.com/redis/docs/tree/main/content/develop/use-cases/pub-sub/go) on GitHub, or grab them with `curl`:\n\n[code example]"
    },
    {
      "id": "start-the-demo-server",
      "title": "Start the demo server",
      "role": "content",
      "text": "Go's `package main` can't live in the same directory as `package pubsub`, so create a tiny `main.go` in a subdirectory that calls the demo entry point:\n\n[code example]\n\nThen build and run:\n\n[code example]\n\nYou should see:\n\n[code example]\n\nOpen [http://127.0.0.1:8097](http://127.0.0.1:8097) in a browser. You can:\n\n* Publish messages of any text to any channel name in any batch size.\n* Add named subscribers that listen on either a specific channel (`orders:new`) or a glob pattern (`notifications:*`). A single subscriber can listen on multiple targets — enter them comma-separated.\n* Watch each subscriber's incoming-message panel update every 800 ms.\n* See the server-side view: `PUBSUB CHANNELS` lists exact-match channels with subscribers, `PUBSUB NUMSUB` gives per-channel counts, and `PUBSUB NUMPAT` counts active pattern subscriptions.\n* Click **Reset** to drop every subscription, zero the counters, and re-seed the three default subscribers.\n\nIf your Redis server is running elsewhere, start the demo with `--redis-host` and `--redis-port`."
    },
    {
      "id": "production-usage",
      "title": "Production usage",
      "role": "content",
      "text": ""
    },
    {
      "id": "pub-sub-is-at-most-once-pair-it-with-durable-state-if-you-need-replay",
      "title": "Pub/sub is at-most-once — pair it with durable state if you need replay",
      "role": "content",
      "text": "A subscriber that's offline when a message is published misses it permanently. For events you can't afford to lose, write the durable record (the order row, the cache key version, the audit log entry) to its primary store, then `PUBLISH` a notification so live consumers can pick it up immediately. On reconnect, consumers reconcile by reading the durable store, not by waiting for missed pub/sub messages. If you actually need replay or at-least-once delivery, switch to [Redis Streams](https://redis.io/docs/latest/develop/data-types/streams) with consumer groups."
    },
    {
      "id": "use-a-separate-connection-or-pubsub-object-per-subscriber",
      "title": "Use a separate connection (or `PubSub` object) per subscriber",
      "role": "content",
      "text": "A go-redis `*redis.PubSub` puts its connection into subscribe-only mode: ordinary commands (`Get`, `HSet`, etc.) using that connection will hang. Every call to `client.Subscribe(...)` / `client.PSubscribe(...)` checks out a fresh connection from the pool, so size the pool generously when you have many subscribers, and don't share one `PubSub` object across business subscribers (closing it would close the channel for all of them)."
    },
    {
      "id": "prefer-redis-pubsub-channel-over-a-hand-written-receivemessage-loop",
      "title": "Prefer `*redis.PubSub.Channel()` over a hand-written `ReceiveMessage` loop",
      "role": "content",
      "text": "`Channel()` runs an internal goroutine that pumps the socket into a buffered Go channel and transparently reconnects on transient failures. A hand-written `for { ReceiveMessage(ctx) }` loop has to handle reconnect, timeouts, and back-pressure itself. Use `Channel()` unless you genuinely need fine-grained control over the read loop, in which case `ReceiveTimeout()` plus your own reconnect logic is the next step down."
    },
    {
      "id": "choose-a-topic-naming-convention-up-front",
      "title": "Choose a topic naming convention up front",
      "role": "content",
      "text": "A flat namespace gets ugly fast — `email`, `email_high_priority`, `email_high_priority_billing`. Pick a colon-separated hierarchy (`notifications:billing:invoice`, `cache:invalidate:products:p-001`) so consumers can subscribe at the right level: a billing service uses `notifications:billing:*`, the audit logger uses `notifications:*`. Glob patterns are evaluated for every published message, so don't go wild with multiple wildcards on hot paths — `*:*:*` matches everything and costs more than a flat `notifications:*` would."
    },
    {
      "id": "don-t-do-heavy-work-in-the-dispatch-goroutine",
      "title": "Don't do heavy work in the dispatch goroutine",
      "role": "content",
      "text": "The dispatch goroutine reads messages from a single Go channel. If your handler blocks (synchronous HTTP call, big computation, slow DB write), the next message waits behind it and the subscriber's effective throughput drops to whatever the handler's latency is. For heavier work, the handler should hand the message off to a worker pool, a buffered job channel, or — for true durable handoff — a [Redis Streams](https://redis.io/docs/latest/develop/data-types/streams) consumer group."
    },
    {
      "id": "tune-the-subscriber-buffer-for-your-traffic-shape",
      "title": "Tune the subscriber buffer for your traffic shape",
      "role": "content",
      "text": "The demo caps each subscriber's in-memory message buffer at 50. That's right for showing the recent activity in a UI, but a real subscriber typically processes each message and discards it — the buffer is only there for human inspection. If you keep a buffer, make sure it's bounded; an unbounded ring on a chatty pattern subscriber will eventually OOM the process. Likewise, `Channel()` accepts a `redis.WithChannelSize(size)` option if you need a larger or smaller internal buffer between the socket reader and your consumer goroutine."
    },
    {
      "id": "sharded-pub-sub-on-a-redis-cluster",
      "title": "Sharded pub/sub on a Redis Cluster",
      "role": "content",
      "text": "On a Redis Cluster, plain `PUBLISH` fans every message out to every node via the cluster bus, which becomes a hotspot at high throughput. Redis 7.0 added [sharded pub/sub](https://redis.io/docs/latest/develop/pubsub#sharded-pubsub): channels are hashed to slots, and `SPUBLISH` / `SSUBSCRIBE` only touch the shard that owns the slot. go-redis exposes the matching `SPublish`, `SSubscribe`, `PubSubShardChannels`, and `PubSubShardNumSub` methods on the cluster client. If you're scaling pub/sub on a cluster, prefer the sharded commands and pick channel names whose hash distribution matches your traffic."
    },
    {
      "id": "inspect-pub-sub-state-directly-in-redis",
      "title": "Inspect pub/sub state directly in Redis",
      "role": "content",
      "text": "Because pub/sub has no keyspace, `KEYS`/`SCAN` won't show you anything. Use the introspection commands instead:\n\n[code example]\n\n`redis-cli` in subscribe mode only exits with `Ctrl-C` — it can't issue any other commands while subscribed."
    },
    {
      "id": "learn-more",
      "title": "Learn more",
      "role": "related",
      "text": "This example uses the following Redis commands:\n\n* [`PUBLISH`](https://redis.io/docs/latest/commands/publish) to fan a message out to every subscriber of a channel.\n* [`SUBSCRIBE`](https://redis.io/docs/latest/commands/subscribe) and [`UNSUBSCRIBE`](https://redis.io/docs/latest/commands/unsubscribe) for exact-match topic subscriptions.\n* [`PSUBSCRIBE`](https://redis.io/docs/latest/commands/psubscribe) and [`PUNSUBSCRIBE`](https://redis.io/docs/latest/commands/punsubscribe) for glob-style pattern subscriptions.\n* [`PUBSUB CHANNELS`](https://redis.io/docs/latest/commands/pubsub-channels) to list channels with at least one active exact-match subscriber.\n* [`PUBSUB NUMSUB`](https://redis.io/docs/latest/commands/pubsub-numsub) to count subscribers per named channel.\n* [`PUBSUB NUMPAT`](https://redis.io/docs/latest/commands/pubsub-numpat) to count active pattern subscriptions server-wide.\n\nSee the [`go-redis` documentation](https://redis.io/docs/latest/develop/clients/go) for full client reference."
    }
  ],
  "examples": [
    {
      "id": "the-pub-sub-hub-helper-ex0",
      "language": "go",
      "code": "import (\n    \"context\"\n\n    \"github.com/redis/go-redis/v9\"\n    \"pubsub\"\n)\n\nclient := redis.NewClient(&redis.Options{Addr: \"localhost:6379\"})\nhub := pubsub.NewRedisPubSubHub(client, 50)\nctx := context.Background()\n\n// Exact-match subscriber\nhub.Subscribe(ctx, \"orders-listener\", []string{\"orders:new\"})\n\n// Pattern subscriber covering an entire topic hierarchy\nhub.PSubscribe(ctx, \"all-notifications\", []string{\"notifications:*\"})\n\n// Publish — returns Redis' delivered count for this PUBLISH\ndelivered, _ := hub.Publish(ctx, \"orders:new\", map[string]any{\n    \"order_id\": 42,\n    \"total\":    199.0,\n})\nfmt.Printf(\"Redis delivered to %d subscriber(s)\\n\", delivered)\n\n// Look at what each subscriber received\nfor _, sub := range hub.Subscriptions() {\n    fmt.Println(sub.Name(), sub.ReceivedTotal(), \"messages\")\n    for _, msg := range sub.Messages(5) {\n        fmt.Println(\"  \", msg.Channel, msg.Payload)\n    }\n}\n\nhub.Unsubscribe(\"orders-listener\")\nhub.Shutdown() // closes every remaining subscription",
      "section_id": "the-pub-sub-hub-helper"
    },
    {
      "id": "data-model-ex0",
      "language": "text",
      "code": "RedisPubSubHub                          (in-process)\n  subscriptions             map[string]*Subscription\n  publishedTotal            int64\n  deliveredTotal            int64\n  channelPublished          map[channel]int\n\nSubscription                            (in-process, one per subscriber)\n  name                      string\n  targets                   []string  (channels or patterns)\n  isPattern                 bool\n  buffer                    []*ReceivedMessage (capped, default 50)\n  received                  int64                (atomic)\n  pubsub                    *redis.PubSub        (owns one connection)\n  goroutine                 reads ps.Channel()",
      "section_id": "data-model"
    },
    {
      "id": "publishing-messages-ex0",
      "language": "go",
      "code": "func (h *RedisPubSubHub) Publish(ctx context.Context, channel string, message interface{}) (int64, error) {\n    payload, err := json.Marshal(message)\n    if err != nil {\n        return 0, err\n    }\n    delivered, err := h.client.Publish(ctx, channel, payload).Result()\n    if err != nil {\n        return 0, err\n    }\n    h.statsMu.Lock()\n    h.publishedTotal++\n    h.deliveredTotal += delivered\n    h.channelPublished[channel]++\n    h.statsMu.Unlock()\n    return delivered, nil\n}",
      "section_id": "publishing-messages"
    },
    {
      "id": "subscribing-to-channels-ex0",
      "language": "go",
      "code": "func (h *RedisPubSubHub) Subscribe(ctx context.Context, name string, channels []string) (*Subscription, error) {\n    return h.register(ctx, name, channels, false)\n}",
      "section_id": "subscribing-to-channels"
    },
    {
      "id": "subscribing-to-channels-ex1",
      "language": "go",
      "code": "var ps *redis.PubSub\nif isPattern {\n    ps = h.client.PSubscribe(ctx, targets...)\n} else {\n    ps = h.client.Subscribe(ctx, targets...)\n}\nsub := &Subscription{\n    name:      name,\n    targets:   targets,\n    isPattern: isPattern,\n    pubsub:    ps,\n    ch:        ps.Channel(),\n    // ...\n}\ngo sub.run()",
      "section_id": "subscribing-to-channels"
    },
    {
      "id": "subscribing-to-channels-ex2",
      "language": "go",
      "code": "func (s *Subscription) dispatch(msg *redis.Message) {\n    var pattern *string\n    if msg.Pattern != \"\" {\n        p := msg.Pattern\n        pattern = &p\n    }\n    var payload interface{}\n    if err := json.Unmarshal([]byte(msg.Payload), &payload); err != nil {\n        payload = msg.Payload\n    }\n    wrapped := &ReceivedMessage{\n        Channel:      msg.Channel,\n        Pattern:      pattern,\n        Payload:      payload,\n        ReceivedAtMs: time.Now().UnixMilli(),\n    }\n    // ... prepend to bounded buffer, increment atomic counter ...\n}",
      "section_id": "subscribing-to-channels"
    },
    {
      "id": "pattern-subscriptions-with-psubscribe-ex0",
      "language": "go",
      "code": "hub.PSubscribe(ctx, \"all-notifications\", []string{\"notifications:*\"})\nhub.PSubscribe(ctx, \"cache-invalidator\", []string{\"cache:invalidate:*\"})",
      "section_id": "pattern-subscriptions-with-psubscribe"
    },
    {
      "id": "pattern-subscriptions-with-psubscribe-ex1",
      "language": "go",
      "code": "type ReceivedMessage struct {\n    Channel      string      `json:\"channel\"`\n    Pattern      *string     `json:\"pattern\"`\n    Payload      interface{} `json:\"payload\"`\n    ReceivedAtMs int64       `json:\"received_at_ms\"`\n}",
      "section_id": "pattern-subscriptions-with-psubscribe"
    },
    {
      "id": "inspecting-active-subscribers-ex0",
      "language": "go",
      "code": "hub.ActiveChannels(ctx, \"*\")                                  // PUBSUB CHANNELS *\nhub.ChannelSubscriberCounts(ctx, []string{\"orders:new\", ...}) // PUBSUB NUMSUB ch1 ch2 ...\nhub.PatternSubscriberCount(ctx)                               // PUBSUB NUMPAT",
      "section_id": "inspecting-active-subscribers"
    },
    {
      "id": "stats-and-history-ex0",
      "language": "go",
      "code": "func (h *RedisPubSubHub) Stats(ctx context.Context) Stats {\n    // ... snapshot counters under statsMu ...\n    subs := h.Subscriptions()\n    var received int64\n    for _, sub := range subs {\n        received += sub.ReceivedTotal()\n    }\n    patternSubs, _ := h.PatternSubscriberCount(ctx)\n    return Stats{\n        PublishedTotal:       published,\n        DeliveredTotal:       delivered,\n        ReceivedTotal:        received,\n        ActiveSubscriptions:  len(subs),\n        ChannelPublished:     perChannel,\n        PatternSubscriptions: patternSubs,\n    }\n}",
      "section_id": "stats-and-history"
    },
    {
      "id": "prerequisites-ex0",
      "language": "text",
      "code": "require github.com/redis/go-redis/v9 v9.18.0",
      "section_id": "prerequisites"
    },
    {
      "id": "get-the-source-files-ex0",
      "language": "bash",
      "code": "mkdir pub-sub-demo && cd pub-sub-demo\nBASE=https://raw.githubusercontent.com/redis/docs/main/content/develop/use-cases/pub-sub/go\ncurl -O $BASE/pubsub_hub.go\ncurl -O $BASE/demo_server.go\ncurl -O $BASE/go.mod\ncurl -O $BASE/go.sum",
      "section_id": "get-the-source-files"
    },
    {
      "id": "start-the-demo-server-ex0",
      "language": "bash",
      "code": "mkdir -p cmd/demo\ncat > cmd/demo/main.go <<'EOF'\npackage main\n\nimport \"pubsub\"\n\nfunc main() { pubsub.RunDemoServer() }\nEOF",
      "section_id": "start-the-demo-server"
    },
    {
      "id": "start-the-demo-server-ex1",
      "language": "bash",
      "code": "go mod tidy\ngo run ./cmd/demo",
      "section_id": "start-the-demo-server"
    },
    {
      "id": "start-the-demo-server-ex2",
      "language": "text",
      "code": "Redis pub/sub demo server listening on http://127.0.0.1:8097\nUsing Redis at localhost:6379\nSeeded 3 default subscription(s)",
      "section_id": "start-the-demo-server"
    },
    {
      "id": "inspect-pub-sub-state-directly-in-redis-ex0",
      "language": "bash",
      "code": "# Which channels currently have at least one exact-match subscriber?\nredis-cli pubsub channels '*'\n\n# How many subscribers does each channel have?\nredis-cli pubsub numsub orders:new notifications:billing chat:lobby\n\n# How many active pattern subscriptions across the whole server?\nredis-cli pubsub numpat\n\n# Subscribe interactively from the CLI to watch traffic on a pattern\nredis-cli psubscribe 'orders:*'",
      "section_id": "inspect-pub-sub-state-directly-in-redis"
    }
  ]
}
