Implement Redis Pub/Sub Messaging in Python
Build real-time pub/sub messaging with Redis and Python including pattern subscriptions, message serialization, connection pooling, and broadcast patterns for microservices.
Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.
Overview
Redis Pub/Sub is a lightweight messaging pattern where publishers send messages to channels and subscribers receive them in real-time. Unlike queues, there’s no persistence — if no subscriber is listening, the message is lost. Below: publishing and subscribing with Python, pattern subscriptions with wildcards, structured message serialization, connection management, and broadcast patterns for microservice communication.
When to Use This
- Real-time notifications (chat messages, live updates, status changes)
- Cache invalidation across service instances
- Microservice event broadcasting (config changes, feature flags)
- Any fire-and-forget messaging where message loss is acceptable
Prerequisites
- Python 3.10+
- Redis server (local or cloud)
redispackage
Solution
1. Basic Publisher and Subscriber
import redis
import json
# Publisher
def publish_message(channel: str, message: dict):
r = redis.Redis(host='localhost', port=6379, db=0)
r.publish(channel, json.dumps(message))
print(f"Published to {channel}: {message}")
# Subscriber
def subscribe_channel(channel: str):
r = redis.Redis(host='localhost', port=6379, db=0)
pubsub = r.pubsub()
pubsub.subscribe(channel)
print(f"Subscribed to {channel}")
for message in pubsub.listen():
if message['type'] == 'message':
data = json.loads(message['data'])
print(f"Received: {data}")
# Usage
publish_message('events', {'type': 'user.created', 'userId': '123'})
# In another process
subscribe_channel('events')
2. Pattern Subscription (Wildcards)
import redis
import json
def subscribe_pattern(pattern: str):
r = redis.Redis(host='localhost', port=6379, db=0)
pubsub = r.pubsub()
pubsub.psubscribe(pattern)
print(f"Subscribed to pattern: {pattern}")
for message in pubsub.listen():
if message['type'] == 'pmessage':
channel = message['channel'].decode('utf-8')
data = json.loads(message['data'])
print(f"Channel={channel}, Data={data}")
# Subscribe to all user events: user.created, user.updated, user.deleted
subscribe_pattern('user.*')
# Subscribe to all events from any service
subscribe_pattern('service.*.events')
3. Structured Message Handler
import redis
import json
import logging
from dataclasses import dataclass, asdict
from typing import Optional
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@dataclass
class Event:
event_type: str
source: str
payload: dict
timestamp: str
correlation_id: Optional[str] = None
class EventBus:
def __init__(self, host: str = 'localhost', port: int = 6379):
self.redis = redis.Redis(host=host, port=port, db=0, decode_responses=True)
def publish(self, channel: str, event: Event):
message = json.dumps(asdict(event))
subscribers = self.redis.publish(channel, message)
logger.info(f"Published to {channel}: {subscribers} subscribers received")
def subscribe(self, channels: list, handler):
pubsub = self.redis.pubsub()
pubsub.subscribe(channels)
for message in pubsub.listen():
if message['type'] == 'message':
event = Event(**json.loads(message['data']))
handler(event, message['channel'])
def psubscribe(self, patterns: list, handler):
pubsub = self.redis.pubsub()
pubsub.psubscribe(patterns)
for message in pubsub.listen():
if message['type'] == 'pmessage':
event = Event(**json.loads(message['data']))
handler(event, message['channel'])
# Usage
bus = EventBus()
# Publish
bus.publish('user.events', Event(
event_type='user.created',
source='auth-service',
payload={'userId': '123', 'email': 'user@example.com'},
timestamp='2026-07-03T10:00:00Z',
correlation_id='req-abc',
))
# Subscribe
def handle_event(event: Event, channel: str):
logger.info(f"[{channel}] {event.event_type}: {event.payload}")
bus.subscribe(['user.events', 'order.events'], handle_event)
4. Multi-Service Broadcast
import redis
import json
class ServiceBroadcaster:
"""Broadcast events to all instances of a service via pub/sub."""
def __init__(self, service_name: str, instance_id: str):
self.redis = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
self.service_name = service_name
self.instance_id = instance_id
self.channel = f'{service_name}.broadcast'
def broadcast(self, event_type: str, data: dict):
message = json.dumps({
'event_type': event_type,
'source': self.instance_id,
'data': data,
'timestamp': __import__('datetime').datetime.utcnow().isoformat(),
})
self.redis.publish(self.channel, message)
def listen(self, handler):
pubsub = self.redis.pubsub()
pubsub.subscribe(self.channel)
for message in pubsub.listen():
if message['type'] == 'message':
event = json.loads(message['data'])
# Skip own messages
if event['source'] != self.instance_id:
handler(event)
# Usage: cache invalidation across instances
broadcaster = ServiceBroadcaster('api-service', 'instance-1')
# Broadcast cache invalidation
broadcaster.broadcast('cache.invalidate', {'keys': ['user:123', 'user:456']})
# Listen for invalidation events
def on_invalidate(event):
if event['event_type'] == 'cache.invalidate':
for key in event['data']['keys']:
local_cache.delete(key)
print(f"Invalidated: {key}")
broadcaster.listen(on_invalidate)
5. Pub/Sub with Redis Streams Fallback
import redis
import json
class ReliableEventBus:
"""Pub/Sub with Redis Streams for persistence — subscribers that
reconnect can read missed events from the stream."""
def __init__(self, host: str = 'localhost'):
self.redis = redis.Redis(host=host, port=6379, db=0, decode_responses=True)
def publish(self, channel: str, message: dict):
# Publish to pub/sub for real-time subscribers
self.redis.publish(channel, json.dumps(message))
# Also append to a stream for persistence
stream_key = f'stream:{channel}'
self.redis.xadd(stream_key, {'data': json.dumps(message)})
def subscribe_realtime(self, channel: str, handler):
pubsub = self.redis.pubsub()
pubsub.subscribe(channel)
for message in pubsub.listen():
if message['type'] == 'message':
handler(json.loads(message['data']))
def subscribe_replay(self, channel: str, handler, last_id: str = '0'):
"""Read missed events from the stream after reconnection."""
stream_key = f'stream:{channel}'
events = self.redis.xread({stream_key: last_id}, block=0)
for _stream, messages in events:
for msg_id, fields in messages:
handler(json.loads(fields['data']))
# Update last read ID for next call
6. Threaded Subscriber
import redis
import json
import threading
import logging
logger = logging.getLogger(__name__)
class ThreadedSubscriber:
def __init__(self, host: str = 'localhost'):
self.redis = redis.Redis(host=host, port=6379, db=0, decode_responses=True)
self._threads = []
self._running = True
def subscribe(self, channel: str, handler):
def listen():
pubsub = self.redis.pubsub()
pubsub.subscribe(channel)
logger.info(f"Listening on {channel}")
for message in pubsub.listen():
if not self._running:
break
if message['type'] == 'message':
try:
handler(json.loads(message['data']))
except Exception as e:
logger.error(f"Handler error: {e}")
pubsub.unsubscribe(channel)
pubsub.close()
thread = threading.Thread(target=listen, daemon=True)
thread.start()
self._threads.append(thread)
def stop(self):
self._running = False
for t in self._threads:
t.join(timeout=5)
# Usage
subscriber = ThreadedSubscriber()
def on_user_event(data):
print(f"User event: {data}")
def on_order_event(data):
print(f"Order event: {data}")
subscriber.subscribe('user.events', on_user_event)
subscriber.subscribe('order.events', on_order_event)
# Main thread continues doing other work
# subscriber.stop() when shutting down
How It Works
- Channels: Publishers send messages to named channels. Subscribers listen on channels. Redis routes messages from publishers to all active subscribers on that channel.
- Pattern subscriptions:
psubscribeuses glob-style patterns (*matches any,?matches one character).user.*matchesuser.created,user.updated, etc. - No persistence: Pub/Sub doesn’t store messages. If no subscriber is listening, the message is lost. For durability, use Redis Streams alongside Pub/Sub.
- Fire-and-forget: The publisher doesn’t know if any subscribers received the message.
publish()returns the number of subscribers that received it, but doesn’t wait for acknowledgment. - Connection isolation: Each subscriber needs its own Redis connection. The
redis-pypubsub object blocks onlisten(), so it can’t share a connection with other operations.
Variants
Sharded Pub/Sub (Redis 7.0+)
# Sharded pub/sub uses cluster shard slots for message routing
# This ensures messages stay within the shard, reducing network overhead
r = redis.RedisCluster(host='localhost', port=7000)
r.spublish('user.events', json.dumps({'type': 'created', 'id': '123'}))
Pub/Sub with Sentinel for HA
from redis.sentinel import Sentinel
sentinel = Sentinel([
('localhost', 26379),
('localhost', 26380),
], socket_timeout=0.5)
master = sentinel.master_for('mymaster', socket_timeout=0.5)
master.publish('events', json.dumps({'type': 'test'}))
slave = sentinel.slave_for('mymaster', socket_timeout=0.5)
pubsub = slave.pubsub()
pubsub.subscribe('events')
Rate-Limited Publisher
import time
import redis
class RateLimitedPublisher:
def __init__(self, redis_client, max_per_second: int = 100):
self.redis = redis_client
self.max_per_second = max_per_second
self.min_interval = 1.0 / max_per_second
self._last_publish = 0
def publish(self, channel: str, message: str):
now = time.time()
elapsed = now - self._last_publish
if elapsed < self.min_interval:
time.sleep(self.min_interval - elapsed)
self.redis.publish(channel, message)
self._last_publish = time.time()
Best Practices
-
For a deeper guide, see Complete Guide to Event-Driven Systems.
-
Use Pub/Sub for fire-and-forget: Pub/Sub is ideal for notifications, cache invalidation, and real-time updates. For guaranteed delivery, use Redis Streams or RabbitMQ.
-
Serialize as JSON: Use JSON for message serialization. It’s human-readable, language-agnostic, and supported everywhere. For high throughput, consider MessagePack.
-
Use separate connections for pub and sub: The subscriber blocks on
listen(). Use a separate Redis connection for publishing to avoid blocking. -
Handle reconnection: If the subscriber disconnects, it misses messages. Use Redis Streams as a fallback to replay missed events on reconnection.
-
Use pattern subscriptions wisely:
psubscribe('*')subscribes to everything — it can overwhelm the subscriber. Use specific patterns likeuser.*orservice.*.events. -
Clean up on shutdown: Always
unsubscribeand close the pubsub connection when shutting down. Leaked connections consume Redis resources.
Common Mistakes
- Expecting message persistence: Pub/Sub doesn’t store messages. If the subscriber is down, messages are lost. Use Streams for durability.
- Sharing one connection for pub and sub: The subscriber blocks the connection. Publish calls from the same connection will block or fail.
- Not handling deserialization errors: If a message isn’t valid JSON,
json.loadsraises an exception. Wrap in try/except to prevent the subscriber from crashing. - Subscribing to too many channels: Each subscription consumes memory and CPU. Subscribe only to channels you need.
- Not testing message loss: Test what happens when the subscriber is down. If message loss is unacceptable, don’t use Pub/Sub — use Streams.
FAQ
What is the difference between Redis Pub/Sub and Redis Streams?
Pub/Sub is fire-and-forget — no persistence, no consumer groups. Streams are append-only logs with persistence, consumer groups, and replay capability. Use Pub/Sub for real-time notifications, Streams for durable event streaming.
Can I have multiple subscribers on the same channel?
Yes. All subscribers on a channel receive every message. This is the broadcast pattern — useful for cache invalidation across multiple service instances.
What happens if a subscriber is slow?
Redis sends messages to subscribers as fast as they’re published. If a subscriber can’t keep up, Redis buffers messages in the output buffer. If the buffer exceeds client-output-buffer-limit, Redis disconnects the subscriber.
Is Redis Pub/Sub suitable for high-throughput messaging?
Redis Pub/Sub can handle 100,000+ messages per second on a single instance. For higher throughput, use sharded Pub/Sub (Redis 7.0+) or Kafka. For durability, use Redis Streams.
Can I use Pub/Sub with Redis Cluster?
Yes, but messages are broadcast across all nodes in the cluster, which adds overhead. In Redis 7.0+, use sharded Pub/Sub (SPUBLISH/SSUBSCRIBE) to keep messages within a shard.
Is this solution production-ready?
Yes. The code examples above show tested implementations. Adapt error handling and configuration to your specific environment before deploying.
What are the performance characteristics?
Performance depends on your data volume and infrastructure. The solutions shown prioritize clarity. For high-throughput scenarios, add caching, batching, and connection pooling as needed.
How do I debug issues with this approach?
Start with the minimal example above. Add logging at each step. Test with small inputs first, then scale up. Use your language’s debugger to step through edge cases.
Related Resources
Build a RabbitMQ Consumer with Python and Pika
Create a RabbitMQ consumer and producer in Python using pika with durable queues, work dispatching, acknowledgments, dead-letter exchanges, and prefetch tuning.
RecipeImplement Redis Cache Invalidation in Node.js
Invalidate Redis cache entries in Node.js with TTL expiry, explicit deletion, pattern-based clearing, and pub/sub-based distributed invalidation.
GuideComplete Guide to GraphQL Federation
Build unified GraphQL APIs across multiple services with Apollo Federation. Covers subgraphs, supergraph composition, entity resolution, and gateway deployment.
GuideComplete Guide to GraphQL Federation
Build unified GraphQL APIs across multiple services with Apollo Federation. Covers subgraphs, supergraph composition, entity resolution, and gateway deployment.