post thumbnail

In-Memory Message Queue with Redis

Discover how Redis enables ultra-fast in-memory messaging using Lists, Pub/Sub, and Kafka-like Streams with consumer groups. Learn Go code examples for producers/consumers and compare Redis with Kafka/RabbitMQ. Perfect for low-latency tasks, async workflows, and lightweight microservices integration. Boost performance with Redis' atomic operations and persistence.

2025-08-28

In the previous articles, [Kafka and the Producer-Consumer Model](https://xx/Kafka and the Producer-Consumer Model) and [RabbitMQ and the Producer-Consumer Model](https://xx/RabbitMQ and the Producer-Consumer Model), we explored two classic implementations of the producer-consumer model: Kafka and RabbitMQ. Kafka excels in high-throughput big data scenarios with its distributed architecture, while RabbitMQ provides reliable messaging and flexible routing in microservice environments. This article focuses on a lighter, faster solution: Redis.

What is Redis?

Redis (REmote DIctionary Server) is an open-source, in-memory key-value data store often used as a cache or fast-response NoSQL database.

It supports various data types, including strings, hashes, lists, sets, and sorted sets, and provides rich operations like push/pop, add/remove, and set intersections, all of which are atomic.

Redis achieves exceptional performance because all data is stored and accessed in memory. It also supports persistence through RDB snapshots and AOF (Append-Only File). Moreover, Redis supports distributed scaling via clustering for handling larger workloads.

Another useful feature of Redis is its built-in pub/sub mechanism, which allows it to act as a producer-consumer system through the PUBLISH and SUBSCRIBE commands. While Redis is not a dedicated message queue, its diverse data structures and in-memory speed make it an excellent lightweight solution for high-performance messaging.

Redis and the Producer-Consumer Model

In [From Basics to Advanced – The Complete Producer-Consumer Model Guide](https://xx/From Basics to Advanced – The Complete Producer-Consumer Model Guide), we learned that the core idea is: producers write to a buffer (queue), and consumers read from it asynchronously, decoupling the two sides.

Redis wasn’t designed exclusively for this model, but it provides several data structures that make it very suitable for implementing it. The three most common Redis-based implementations are:

1. List-based Queue

Redis Lists are implemented as doubly linked lists. Producers can LPUSH items to the list, and consumers can BRPOP (blocking pop) items off. This is the simplest and most widely used Redis queue pattern.

2. Pub/Sub

Redis supports native publish/subscribe messaging: producers PUBLISH to a channel, and consumers SUBSCRIBE to one or more channels.
However, messages are not persisted—if a subscriber disconnects or is offline, it will miss any published messages.

3. Streams

Redis Streams provide a log-like data structure similar to Kafka.
They support features such as persistence, replication, consumer groups, message acknowledgment, and message tracking. Streams are the officially recommended Redis structure for messaging use cases.

These options make Redis a powerful tool for lightweight messaging, especially in scenarios where low latency is critical.

Simulating Producers and Consumers

Let’s implement two Redis-based queues in Go: one using List, and the other using Stream.

List-Based Queue

Producer:

package main

import (
    "context"
    "fmt"
    "github.com/redis/go-redis/v9"
)

var ctx = context.Background()

func main() {
    rdb := redis.NewClient(&redis.Options{
        Addr: "localhost:6379",
        DB:   0,
    })

    for i := 1; i <= 10; i++ {
        data := fmt.Sprintf("data-%d", i)
        err := rdb.LPush(ctx, "list-queue", data).Err()
        if err != nil {
            fmt.Printf("Failed to produce: %v\n", err)
            continue
        }
        fmt.Printf("Produced: %s\n", data)
    }
}

Consumer:

package main

import (
    "context"
    "fmt"
    "github.com/redis/go-redis/v9"
    "time"
)

var ctx = context.Background()

func main() {
    rdb := redis.NewClient(&redis.Options{
        Addr: "localhost:6379",
        DB:   0,
    })

    for {
        result, err := rdb.BRPop(ctx, 5*time.Second, "list-queue").Result()
        if err == redis.Nil {
            fmt.Println("No data found, waiting...")
            continue
        } else if err != nil {
            fmt.Printf("Error consuming data: %v\n", err)
            break
        }

        if len(result) == 2 {
            fmt.Printf("Consumed: %s\n", result[1])
        }
    }
}

Stream-Based Queue

Producer:

package main

import (
    "context"
    "fmt"
    "github.com/redis/go-redis/v9"
    "strconv"
    "time"
)

var ctx = context.Background()

func main() {
    rdb := redis.NewClient(&redis.Options{
        Addr: "localhost:6379",
        DB:   0,
    })

    streamKey := "stream-queue"

    for i := 1; i <= 10; i++ {
        id, err := rdb.XAdd(ctx, &redis.XAddArgs{
            Stream: streamKey,
            Values: map[string]interface{}{
                "order_id": fmt.Sprintf("OID-%03d", i),
                "amount":   strconv.Itoa(100 + i),
            },
        }).Result()

        if err != nil {
            fmt.Printf("Failed to produce: %v\n", err)
        } else {
            fmt.Printf("Produced message ID: %s\n", id)
        }

        time.Sleep(500 * time.Millisecond)
    }
}

Consumer (Consumer Group):

package main

import (
    "context"
    "fmt"
    "github.com/redis/go-redis/v9"
    "time"
)

var ctx = context.Background()

func main() {
    rdb := redis.NewClient(&redis.Options{
        Addr: "localhost:6379",
        DB:   0,
    })

    streamKey := "stream-queue"
    groupName := "mygroup"
    consumerName := "consumer-1"

    err := rdb.XGroupCreateMkStream(ctx, streamKey, groupName, "$").Err()
    if err != nil && err.Error() != "BUSYGROUP Consumer Group name already exists" {
        panic(err)
    }

    fmt.Println("Start consuming...")

    for {
        streams, err := rdb.XReadGroup(ctx, &redis.XReadGroupArgs{
            Group:    groupName,
            Consumer: consumerName,
            Streams:  []string{streamKey, ">"},
            Count:    5,
            Block:    5 * time.Second,
        }).Result()

        if err == redis.Nil {
            continue
        } else if err != nil {
            fmt.Printf("ReadGroup error: %v\n", err)
            time.Sleep(time.Second)
            continue
        }

        for _, stream := range streams {
            for _, msg := range stream.Messages {
                fmt.Printf("Consumed ID: %s, Values: %v\n", msg.ID, msg.Values)
                if err := rdb.XAck(ctx, streamKey, groupName, msg.ID).Err(); err != nil {
                    fmt.Printf("ACK failed: %v\n", err)
                }
            }
        }
    }
}

Conclusion

Redis is not a dedicated message queue, but thanks to its powerful data structures and high-speed in-memory operations, it can effectively serve as a lightweight messaging platform.

By using Lists or Streams, Redis offers flexible, fast, and simple implementations of the producer-consumer model, making it a great choice for scenarios involving:

Compared with Kafka and RabbitMQ, Redis stands out in lightweight, low-cost, and high-speed use cases.