Redis offers multiple data structures for message passing between services. Two common patterns are Lists (with LPUSH/RPOP operations) and Pub/Sub channels. While both can transport messages, they serve different use cases and have distinct characteristics. This guide explores when to use each pattern, with practical Go examples.
Redis Lists implement a queue data structure. Messages are pushed onto one end of the list and popped from the other, providing FIFO (first-in-first-out) semantics with persistence.
Lists are ideal when you need:
Examples: Background job processing, task distribution to workers, email queue, audit log ingestion.
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"time"
"github.com/redis/go-redis/v9"
)
type Message struct {
ID string `json:"id"`
Content string `json:"content"`
Timestamp time.Time `json:"timestamp"`
}
func main() {
ctx := context.Background()
// Connect to Redis
client := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
defer client.Close()
// Create a message
msg := Message{
ID: "msg-001",
Content: "Process this task",
Timestamp: time.Now(),
}
// Marshal to JSON
jsonData, err := json.Marshal(msg)
if err != nil {
log.Fatal(err)
}
// Push to list (queue name: "work_queue")
err = client.LPush(ctx, "work_queue", jsonData).Err()
if err != nil {
log.Fatal(err)
}
fmt.Println("Message pushed to queue")
}
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"time"
"github.com/redis/go-redis/v9"
)
type Message struct {
ID string `json:"id"`
Content string `json:"content"`
Timestamp time.Time `json:"timestamp"`
}
func main() {
ctx := context.Background()
client := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
defer client.Close()
fmt.Println("Waiting for messages...")
for {
// Blocking pop with 5-second timeout
result, err := client.BRPop(ctx, 5*time.Second, "work_queue").Result()
if err == redis.Nil {
// Timeout, no messages available
continue
}
if err != nil {
log.Printf("Error: %v", err)
continue
}
// result[0] is the queue name, result[1] is the message
var msg Message
err = json.Unmarshal([]byte(result[1]), &msg)
if err != nil {
log.Printf("Failed to unmarshal: %v", err)
continue
}
fmt.Printf("Received: %+v\n", msg)
// Process the message here
time.Sleep(1 * time.Second) // Simulate processing
}
}
Pub/Sub implements a message broker pattern where publishers send messages to channels, and all subscribed consumers receive copies of each message in real-time.
Pub/Sub excels when you need:
Examples: Live dashboard updates, chat applications, real-time analytics, cache invalidation, monitoring alerts.
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"time"
"github.com/redis/go-redis/v9"
)
type Event struct {
Type string `json:"type"`
Payload string `json:"payload"`
Timestamp time.Time `json:"timestamp"`
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
client := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
defer client.Close()
// Create an event
event := Event{
Type: "sms.incoming",
Payload: "New SMS received",
Timestamp: time.Now(),
}
// Marshal to JSON
jsonData, err := json.Marshal(event)
if err != nil {
log.Fatal(err)
}
// Publish to channel
channelName := "sms:incoming"
err = client.Publish(ctx, channelName, jsonData).Err()
if err != nil {
log.Printf("Failed to publish: %v", err)
return
}
fmt.Printf("Event published to channel: %s\n", channelName)
}
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"github.com/redis/go-redis/v9"
)
type Event struct {
Type string `json:"type"`
Payload string `json:"payload"`
Timestamp time.Time `json:"timestamp"`
}
func main() {
ctx := context.Background()
client := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
defer client.Close()
// Subscribe to channel
pubsub := client.Subscribe(ctx, "sms:incoming")
defer pubsub.Close()
// Wait for subscription confirmation
_, err := pubsub.Receive(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Println("Subscribed to sms:incoming channel")
// Process messages
ch := pubsub.Channel()
for msg := range ch {
var event Event
err := json.Unmarshal([]byte(msg.Payload), &event)
if err != nil {
log.Printf("Failed to unmarshal: %v", err)
continue
}
fmt.Printf("Received event: %+v\n", event)
// Process the event here
}
}
| Feature | Lists (LPUSH/RPOP) | Pub/Sub |
|---|---|---|
| Persistence | Yes, until consumed | No, ephemeral |
| Delivery | Exactly once per message | Zero or more times (all subscribers) |
| Consumer Model | Competing consumers (load balancing) | Broadcasting (all subscribers receive) |
| Message Retention | Until explicitly removed | Immediate discard after delivery |
| Ordering | FIFO guaranteed | Order preserved per channel |
| Backpressure | Yes, queues build up | No, slow consumers miss messages |
| Disconnection | Messages wait for consumer | Missed messages during downtime |
| Use Case | Task queues, job processing | Event broadcasting, notifications |
In the Mailer-Go project, I initially used Redis Lists to queue incoming SMS messages. The implementation looked like this:
// Original implementation with Lists
func PushToRedis(cfg config.Config, folderName string, sms SMSMessage) {
if !cfg.RedisEnabled || redisClient == nil {
return
}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
jsonData, _ := json.Marshal(sms)
queueName := fmt.Sprintf("sms:%s", folderName)
err := redisClient.LPush(ctx, queueName, string(jsonData)).Err()
if err != nil {
log.Printf("Failed to push to Redis queue: %v", err)
}
}
This worked fine for simple cases, but I ran into a limitation when integrating with n8n (a workflow automation tool). n8n’s Redis Trigger node only supports Pub/Sub, not Lists. This forced a migration to Pub/Sub:
// Updated implementation with Pub/Sub
func PushToRedis(cfg config.Config, folderName string, sms SMSMessage) {
if !cfg.RedisEnabled || redisClient == nil {
return
}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
jsonData, err := json.Marshal(sms)
if err != nil {
log.Printf("Failed to marshal SMS: %v", err)
return
}
channelName := fmt.Sprintf("sms:%s", folderName)
err = redisClient.Publish(ctx, channelName, string(jsonData)).Err()
if err != nil {
log.Printf("Failed to publish to Redis: %v", err)
} else {
log.Printf("Published to Redis channel: %s", channelName)
}
}
For this use case, Pub/Sub was actually a better fit:
If the requirement was “ensure each SMS is processed exactly once by a worker pool,” Lists would have been the right choice. But for “notify all interested systems when an SMS arrives,” Pub/Sub is superior.
Ask yourself:
Use Lists if:
Use Pub/Sub if:
Redis Lists and Pub/Sub solve different problems. Lists provide durable, queue-based messaging for work distribution, while Pub/Sub enables real-time event broadcasting. Understanding their trade-offs helps you choose the right pattern for your use case.
For task processing where you need guaranteed delivery, use Lists. For real-time notifications where you need to inform multiple services simultaneously, use Pub/Sub. In many systems, you’ll use both: Lists for reliable job queues, and Pub/Sub for live status updates.