Chapter 16 · Pub/Sub, Keyspace Notifications, and Messaging Tradeoffs

At-Most-Once Delivery, Disconnect Loss, Slow Subscribers, and Why Pub/Sub Is Not a Durable Queue

Prove Redis Pub/Sub at-most-once delivery with an intentional disconnect, bound slow-subscriber behavior, and show why no subscription cursor, acknowledgment, or replay state exists.

Advanced170–230 minutesDisconnect loss, slow subscribers, and at-most-once deliveryRedis Open Source 8.10.1redis-py 8.1.0Free/local-firstLast reviewed: September 6, 2026

Learning outcomes

AtlasMart now understands channels, but a dangerous assumption remains: “if Redis accepted the publication, every notification worker will eventually process it.” This lesson breaks that assumption with a controlled disconnect and shows how a slow subscriber can accumulate buffers even though the publisher has no acknowledgment of downstream business processing.

01

Prove at-most-once disconnect loss with a deterministic subscriber/publisher sequence.

02

Separate Redis publication delivery from application callback completion and external side effects.

03

Inspect Pub/Sub client state/output-buffer indicators without forcing unsafe buffer-limit changes.

04

Bound application-side subscriber queues and explain why slow consumers require backpressure or a different messaging primitive.

05

Compare the same disconnected interval with a Redis Stream entry that remains available for replay.

Exact lab baseline

All Chapter 16 mandatory labs reuse the disposable Chapter 01 environment: Redis Open Source 8.10.1 from pinned Docker image redis:8.10.1, container atlasmart-redis-ch01, standalone topology, endpoint 127.0.0.1:6379, TLS disabled only because traffic is loopback-local, default ACL user disabled, named academy-admin and atlasmart-app users, logical database 0, AOF with appendfsync everysec plus RDB snapshots, persistent /data, and no explicit maxmemory/eviction policy. Python examples target redis-py 8.1.0. The Chapter 01 application ACL intentionally does not assume Pub/Sub command permissions, so Pub/Sub/configuration demonstrations use academy-admin until Lesson 5 creates a temporary least-privilege Chapter 16 user. Fixtures stay under atlasmart:ch16:.

1. “Sent” is not “processed”

PUBLISH returning a positive count means Redis sent a message toward currently matching subscribers. It does not mean their callback succeeded, a downstream API completed, or the notification was durably recorded. There is no Pub/Sub acknowledgment command. If a process crashes after Redis pushes a message but before business processing finishes, Redis will not redeliver it.

Correctness rule

Never make a non-repeatable business transition depend solely on receiving a Pub/Sub message. Write durable state first; use Pub/Sub only to reduce time-to-observe that state.

2. Deterministic disconnect-loss experiment

The following redis-py script subscribes, receives one live message, closes the subscription, publishes two messages during the gap, then creates a fresh subscription and publishes one more. The expected observation is sequence 1 and 4 only. The exact subscriber confirmation frames vary by redis-py/RESP response mode, so the code drains confirmations instead of treating their shape as business data.

Python · prove messages 2 and 3 are unreplayable
import time, redisr = redis.Redis(host="127.0.0.1", port=6379, username="academy-admin", password="AtlasMart-Admin-Lab-Only-2026", protocol=3, decode_responses=True)channel = "atlasmart:ch16:l2:disconnect"def wait_message(ps, timeout=1.5):    end = time.monotonic() + timeout    while time.monotonic() < end:        m = ps.get_message(ignore_subscribe_messages=True, timeout=0.1)        if m is not None:            return m    return Noneps = r.pubsub()ps.subscribe(channel)time.sleep(0.05)r.publish(channel, "seq=1")print("online", wait_message(ps))ps.close()print("publish_while_offline", r.publish(channel, "seq=2"), r.publish(channel, "seq=3"))ps = r.pubsub()ps.subscribe(channel)time.sleep(0.05)print("unexpected_replay", wait_message(ps, 0.30))  # expected Noner.publish(channel, "seq=4")print("future", wait_message(ps))ps.close()# Expected: seq=1 and seq=4 are visible; seq=2/3 are not replayed.

3. Live subscriber count explains the loss window

During the disconnected interval, PUBSUB NUMSUB should report zero exact subscribers for the test channel. A subsequent PUBLISH can therefore return zero. That is concrete evidence that no active connection was present; it is not a durable delivery ledger and cannot tell you which historical messages a former subscriber missed.

redis-cli · observe live routing state
PUBSUB NUMSUB atlasmart:ch16:l2:disconnectPUBLISH atlasmart:ch16:l2:disconnect probe-with-no-subscriber# If no exact subscriber is live, NUMSUB and PUBLISH should report zero recipients in this standalone fixture.

4. Slow subscribers create queueing and buffer pressure

Redis pushes messages asynchronously; it does not wait for application work after each push. If a subscriber reads the socket slowly, data can accumulate in socket/server output buffers until configured Pub/Sub output-buffer limits or network failure disconnect the client. If the process reads Redis promptly but queues work internally faster than workers consume it, the application can instead exhaust its own memory. Those are different pressure points and should be measured separately.

redis-cli · record Chapter 16 server context
docker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin PINGdocker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin INFO serverdocker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin INFO clientsdocker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin CONFIG GET appendonly appendfsync maxmemory maxmemory-policy notify-keyspace-events client-output-buffer-limitdocker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin ACL WHOAMI
Do not “test” by weakening limits globally

This lesson observes client-output-buffer-limit with CONFIG GET but does not reduce it or flood the server until forced disconnect. Production thresholds are configuration/capacity decisions, not a classroom constant.

5. Bounded slow-consumer harness

This fixture creates one subscriber thread that intentionally sleeps after each message while a publisher sends 60 small messages. The application keeps only a bounded deque of the most recent 20 observations. It demonstrates that “slow consumer” is an application design condition without trying to exhaust Redis or the host. Record processed count and elapsed time on your machine; do not compare those values across environments as a Redis benchmark.

Python · bounded slow subscriber
import collections, threading, time, redisr = redis.Redis(host="127.0.0.1", port=6379, username="academy-admin", password="AtlasMart-Admin-Lab-Only-2026", decode_responses=True)channel = "atlasmart:ch16:l2:slow"recent = collections.deque(maxlen=20)done = threading.Event()def consume():    ps = r.pubsub()    ps.subscribe(channel)    processed = 0    deadline = time.monotonic() + 5    while processed < 60 and time.monotonic() < deadline:        m = ps.get_message(ignore_subscribe_messages=True, timeout=0.1)        if not m: continue        recent.append(m["data"])        processed += 1        time.sleep(0.01)  # intentionally slower callback    print("processed", processed, "recent_buffer", len(recent))    ps.close(); done.set()t = threading.Thread(target=consume, daemon=True); t.start()time.sleep(0.10)for i in range(60): r.publish(channel, f"m{i}")done.wait(6)# Bounded local buffer prevents the demonstration itself from becoming an unbounded-memory pattern.

6. CLIENT LIST is evidence, not a universal field contract

Name subscriber connections when possible and inspect CLIENT LIST TYPE pubsub. Current Redis exposes output-buffer/client-state fields whose exact names and interpretation can evolve; record the raw row with server version. A transient zero buffer on loopback can simply mean the kernel/application drained data quickly. Use sustained representative load to size production limits.

redis-cli · inspect Pub/Sub clients while the slow harness runs
CLIENT LIST TYPE pubsubCONFIG GET client-output-buffer-limit# Capture client name/id, idle time, output-buffer indicators, and server version together.# Do not treat one loopback snapshot as a capacity test.

7. Same outage interval, different Stream behavior

A Redis Stream is a stored data structure. If AtlasMart appends entries while no consumer is running, the entries remain addressable until retention/deletion removes them. A consumer can replay by ID, and consumer groups add pending/acknowledgment state. That is why Streams can support at-least-once processing patterns while Pub/Sub cannot.

redis-cli · durable comparison fixture
XADD atlasmart:ch16:l2:stream '*' seq 2 payload offline-event-2XADD atlasmart:ch16:l2:stream '*' seq 3 payload offline-event-3XLEN atlasmart:ch16:l2:streamXRANGE atlasmart:ch16:l2:stream - +# These entries remain readable after the writer/reader disconnect window, unlike Pub/Sub publications.

8. Failure/misuse: infinite retries do not create durability

Reconnecting a Pub/Sub client aggressively cannot recover publications that occurred while it was absent. Infinite reconnect loops can also amplify outages with connection storms. Use bounded backoff/jitter and a reconciliation step: after reconnect, read authoritative state or replay a durable Stream/broker log from a known cursor.

Failure Pub/Sub-only outcome Repair
Subscriber crash after push message may be lost durable state + idempotent reconciliation
Network disconnect publications during gap lost re-read state or replay durable log
Slow callback buffers/lag grow, possible disconnect bound queue, scale work, or durable consumer model
Publisher retry duplicate application signals possible idempotent payload/version handling

9. Verification and cleanup

  • The disconnect harness observed no replay after reconnect.
  • Subscriber counts were interpreted as live state only.
  • The slow-consumer fixture bounded its own application buffer and message count.
  • No output-buffer limit was weakened and no host networking was manipulated.
  • The Stream entries remained addressable after publication time.
redis-cli · bounded Chapter 16 cleanup
UNLINK atlasmart:ch16:l2:stream

10. Production judgment

Use Pub/Sub when the subscriber can tolerate gaps and reconstruct current state. If each work item must eventually be handled, move the requirement to Streams or another broker with explicit persistence and consumer-state semantics. Under Sentinel or Cluster failover, assume subscribers can disconnect and miss publications during reconnect unless your specific managed/client topology proves otherwise. Track reconnects, subscriber callback latency, client/output buffers, dropped local work, and reconciliation success.

Check your understanding

  1. What does a positive PUBLISH return prove?
  2. Can reconnecting replay missed Pub/Sub messages?
  3. Why not lower client-output-buffer-limit for a classroom failure?
  4. What does a Stream add?
  5. What should happen after Pub/Sub reconnect?
Review the answers

Redis sent the publication toward currently matching subscribers; it does not prove application processing or durable completion.

No. Reconnection establishes a new live subscription only.

It changes server-wide behavior and can affect unrelated clients; bounded observation is safer.

Persisted entries and replay; consumer groups can also add pending ownership and acknowledgment state.

Reconcile from authoritative state or a replayable event source instead of trusting the live channel to fill the gap.

11. Summary and next step

The disconnect experiment turns at-most-once from a slogan into evidence: offline publications disappear. Slow subscribers introduce separate server-socket and application-queue pressure. Lesson 3 applies the same skepticism to keyspace notifications, which are generated automatically but still ride on the same lossy Pub/Sub mechanism.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.