Redis Pub/Sub lets applications send messages to named channels and have Redis deliver them instantly to every client subscribed to those channels. It is a good fit for live notifications, cache invalidation and chat-style updates in applications that already use Redis. In this tutorial you will install Redis on Ubuntu 24.04, practice the Pub/Sub commands, create ACL users that can only publish or only subscribe to specific channels, and build a small Python notification listener. You will also see where Pub/Sub stops being the right tool and Redis Streams should be used instead.
Prerequisites
To follow this tutorial you need:
- A server running Ubuntu 24.04 LTS, for example a CubePath VPS.
- A non-root user with
sudoprivileges. - Basic familiarity with
redis-cliis helpful but not required.
Step 1 - Installing Redis
Install the Redis server and command line tools from the Ubuntu repositories:
sudo apt update
sudo apt install -y redis-server
The package enables and starts the redis-server service, listening only on 127.0.0.1 and ::1. Check that it responds:
redis-cli ping
PONG
Step 2 - Publishing and subscribing
Pub/Sub uses three commands: SUBSCRIBE to listen on one or more channels, PUBLISH to send a message to a channel, and PSUBSCRIBE to listen on every channel that matches a glob pattern. Open a first terminal and subscribe to the notifications channel:
redis-cli SUBSCRIBE notifications
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "notifications"
3) (integer) 1
The connection is now in subscriber mode and waits for messages. In a second terminal, publish one:
redis-cli PUBLISH notifications "Maintenance at 02:00 UTC"
(integer) 1
The return value is the number of clients that received the message. The first terminal shows it:
1) "message"
2) "notifications"
3) "Maintenance at 02:00 UTC"
Now publish to a channel nobody listens to:
redis-cli PUBLISH alerts "Disk almost full"
(integer) 0
Nobody received it, and Redis did not store it. This is the most important property of Pub/Sub: messages are fire and forget. A subscriber that is offline, or connects one second later, never sees them.
Pattern subscriptions
Naming channels hierarchically, for example user:42:notifications, lets a service subscribe to a whole family of channels with one pattern. Stop the first subscriber with CTRL+C and run:
redis-cli PSUBSCRIBE 'user:*:notifications'
Publish to two different users from the second terminal:
redis-cli PUBLISH user:42:notifications "Your invoice is ready"
redis-cli PUBLISH user:7:notifications "New login from Madrid"
Pattern messages include the pattern that matched, followed by the real channel:
1) "pmessage"
2) "user:*:notifications"
3) "user:42:notifications"
4) "Your invoice is ready"
Inspecting active channels
The PUBSUB command shows what is being listened to. With the pattern subscriber still running:
redis-cli PUBSUB NUMPAT
redis-cli PUBSUB CHANNELS '*'
redis-cli PUBSUB NUMSUB notifications
NUMPAT counts pattern subscriptions, CHANNELS lists channels with at least one direct subscriber, and NUMSUB returns the number of subscribers per channel. These commands are useful to confirm that your consumers are actually connected before you blame the publisher.
Step 3 - Restricting channels with ACL users
By default, Ubuntu's Redis has a single default user with no password and access to everything. Any process that can reach the port can publish fake notifications or read every channel. Redis ACLs let you create users limited to specific commands and channel patterns (the &pattern rule).
Open the Redis configuration file:
sudo nano /etc/redis/redis.conf
Add these lines at the end of the file. Replace the three passwords with long random strings, for example generated with openssl rand -base64 32:
# Administrator: full access
user default on >your_admin_password ~* &* +@all
# Backend that publishes notifications
user notifier on >your_notifier_password resetchannels &user:*:notifications -@all +publish +ping
# Service that only listens for notifications
user listener on >your_listener_password resetchannels &user:*:notifications -@all +subscribe +psubscribe +unsubscribe +punsubscribe +ping
What each rule means:
onenables the user and>passwordadds a password.~*grants access to all keys. The two Pub/Sub users have no~rule, so they cannot read or write any key.resetchannels &user:*:notificationsremoves any channel access and then allows only channels matchinguser:*:notifications.-@all +publish ...starts from no commands and adds only the ones listed.pingis allowed so clients and health checks can test the connection.
Restart Redis to apply the configuration:
sudo systemctl restart redis-server
Unauthenticated commands are now rejected:
redis-cli ping
(error) NOAUTH Authentication required.
Test the listener user. --askpass prompts for the password instead of leaving it in your shell history:
redis-cli --user listener --askpass PSUBSCRIBE 'user:*:notifications'
From a second terminal, publish as notifier:
redis-cli --user notifier --askpass PUBLISH user:42:notifications "Payment received"
(integer) 1
Now confirm the restrictions. The notifier cannot publish outside its channel pattern, and the listener cannot publish at all:
redis-cli --user notifier --askpass PUBLISH admin:broadcast "test"
redis-cli --user listener --askpass PUBLISH user:42:notifications "test"
Both commands fail with an error that starts with NOPERM. The exact wording depends on the Redis version:
(error) NOPERM this user has no permissions to access one of the channels used as arguments
(error) NOPERM this user has no permissions to run the 'publish' command
Step 4 - Building a notification listener in Python
In a real application, the listener is a long-running service that receives events and forwards them, for example to WebSocket clients. Install the redis Python client in a virtual environment:
sudo apt install -y python3-venv
python3 -m venv ~/pubsub-demo
~/pubsub-demo/bin/pip install redis
Create the listener:
nano ~/listener.py
import json
import os
import redis
r = redis.Redis(
host="127.0.0.1",
port=6379,
username="listener",
password=os.environ["REDIS_PASSWORD"],
decode_responses=True,
)
pubsub = r.pubsub(ignore_subscribe_messages=True)
pubsub.psubscribe("user:*:notifications")
print("Listening on user:*:notifications")
for message in pubsub.listen():
user_id = message["channel"].split(":")[1]
try:
payload = json.loads(message["data"])
except json.JSONDecodeError:
payload = {"text": message["data"]}
print(f"user={user_id} payload={payload}")
ignore_subscribe_messages=True hides the subscription confirmations, so the loop only receives real messages. The password is read from an environment variable rather than being written in the code.
Create the publisher:
nano ~/notify.py
import json
import os
import sys
import redis
r = redis.Redis(
host="127.0.0.1",
username="notifier",
password=os.environ["REDIS_PASSWORD"],
)
user_id, text = sys.argv[1], sys.argv[2]
receivers = r.publish(f"user:{user_id}:notifications", json.dumps({"text": text}))
print(f"Delivered to {receivers} subscriber(s)")
Start the listener in one terminal. read -s asks for the password without echoing it:
read -rs REDIS_PASSWORD && export REDIS_PASSWORD
~/pubsub-demo/bin/python ~/listener.py
In another terminal, export the notifier password the same way and send a notification:
read -rs REDIS_PASSWORD && export REDIS_PASSWORD
~/pubsub-demo/bin/python ~/notify.py 42 "Your server is ready"
Delivered to 1 subscriber(s)
The listener prints:
user=42 payload={'text': 'Your server is ready'}
Check the return value of publish in your application: 0 means no listener was connected and the event was dropped.
Step 5 - Understanding the limits of Pub/Sub
Pub/Sub keeps no history and has no acknowledgments. Keep these behaviors in mind:
- Offline subscribers miss messages. If the listener restarts, everything published during the restart is lost.
- Slow subscribers get disconnected. Redis buffers outgoing messages per subscriber. The default limit
client-output-buffer-limit pubsub 32mb 8mb 60closes a subscriber whose buffer exceeds 32 MB, or stays above 8 MB for 60 seconds, to protect the server's memory. - Every subscriber gets every message. There is no built-in way to split work between several workers.
When you need delivery guarantees or work sharing, use a Redis Stream instead. A stream stores messages as an append-only log, and consumer groups track which messages each worker has acknowledged. The same notification with a stream looks like this (run as the default user, since streams use keys):
redis-cli --askpass XADD notifications '*' user 42 text "Your server is ready"
redis-cli --askpass XGROUP CREATE notifications mailers 0
redis-cli --askpass XREADGROUP GROUP mailers worker-1 COUNT 10 STREAMS notifications '>'
The message stays in the stream until you trim it, a worker that was offline reads it when it comes back, and XACK marks it as processed.
| Pub/Sub | Streams | |
|---|---|---|
| Stores messages | No | Yes, until trimmed |
| Offline consumers | Miss messages | Catch up later |
| Acknowledgments | No | Yes (XACK) |
| Work sharing | No, all subscribers get everything | Consumer groups |
| Best for | Live updates, cache invalidation | Jobs, events that must not be lost |
Troubleshooting
NOAUTH Authentication required. The client did not authenticate. Pass --user and --askpass to redis-cli, or username and password to your client library.
NOPERM errors on PUBLISH or SUBSCRIBE. The command is not in the user's allowed list, or the channel name does not match the user's & pattern. Check with redis-cli --askpass ACL GETUSER notifier as the default user.
Redis does not start after editing redis.conf. Run sudo journalctl -u redis-server -n 30. A typo in a user line stops Redis from loading the configuration, and the log names the line.
The listener stops receiving after a while. It was probably disconnected for exceeding the output buffer limit. Check the Redis log (/var/log/redis/redis-server.log) for scheduled to be closed ASAP for overcoming of output buffer limits and make the listener process messages faster.
Conclusion
You installed Redis on Ubuntu 24.04, used PUBLISH, SUBSCRIBE and PSUBSCRIBE to send real-time messages, locked down publishers and subscribers to their own channel patterns with ACLs, and built a Python listener. As next steps, run the listener as a systemd service so it restarts automatically, push its messages to browsers through a WebSocket server, and move any event that must not be lost to Redis Streams with consumer groups.
