Real-Time Push Notifications

Channel-based pub/sub over WebSocket with HTTP polling fallback

Subscribe to gateway channels and receive real-time messages — WebSocket first, polling when blocked.

python
from praisonaiagents import Agent
from praisonaiagents.push import PushClient

agent = Agent(
    name="alerts-agent",
    instructions="Summarise incoming alert events in one sentence",
)

client = PushClient("ws://localhost:8765/ws", auth_token="my-token")
await client.connect()

@client.on("channel_message")
async def on_msg(msg):
    print(agent.start(f"Summarise: {msg.data}"))

await client.subscribe("alerts")
await client.wait_closed()

The user subscribes to a channel; push events arrive over WebSocket and the agent summarises each message.

Diagram

subscribe

publish

push

poll fallback

Client

Gateway

Publisher

Client

How It Works

Diagram
RealTimePushAgentUserRealTimePushAgentUserRequestProcessResultResponse

Quick Start

  1. Simple Usage

    Connect and subscribe:

    python
    from praisonaiagents.push import PushClient
    
    client = PushClient("ws://localhost:8765/ws", auth_token="my-token")
    await client.connect()
    
    @client.on("channel_message")
    async def on_message(msg):
        print(f"{msg.channel}: {msg.data}")
    
    await client.subscribe("alerts")
    await client.wait_closed()
  2. With Configuration

    Enable push on the gateway:

    python
    from praisonaiagents import GatewayConfig
    from praisonaiagents.gateway import PushConfig
    
    config = GatewayConfig(push=PushConfig(enabled=True))

How It Works

Diagram
Real-Time Push NotificationsAgentUserReal-Time Push NotificationsAgentUserRequestProcess requestResultResponse
ComponentPurpose
PushClientAuto-reconnect, transport fallback
WebSocketTransportPrimary real-time transport
PollingTransportFallback for restricted networks — durable by default when paired with a delivery store
ChannelsNamed pub/sub streams
PushConfigOpt-in gateway toggle (off by default)

Import paths:

python
from praisonai.push import PushClient                    # wrapper (recommended)
from praisonaiagents.push import PushClient              # lazy re-export
from praisonaiagents.push import ChannelMessage          # core dataclass

After fallback: producer contract

Polling is subscribe-only. After a WebSocket → polling fallback, publish(), create_channel(), and get_presence() raise NotImplementedError — the polling contract has no matching server routes. Gate producer code on PushClient.supports_publish before calling any of them.

python
await client.connect()
if client.supports_publish:
    await client.publish("alerts", {"kind": "info", "text": "hi"})
else:
    # WS blocked → subscribe-only mode. Publish elsewhere.
    logger.warning("Push client in polling fallback; skipping publish.")

supports_publish is True only when a transport is connected and is not the polling fallback. It is False before connect(), after disconnect(), and after a WS → polling fallback.

The client also emits a WARNING log the moment it falls back, so an operator watching the log knows publish is unavailable without instrumenting every call site:

PushClient fell back to polling transport. Unavailable operations under polling:
publish, create_channel, get_presence (they will raise NotImplementedError).
Gate producer code on the supports_publish property.

HA & cross-instance delivery

With PushConfig(redis=...), multiple gateway instances fan out channel messages through Redis pub/sub so a subscriber on any instance receives every publish.

Diagram

HA fan-out

📤 Publisher

🛰️ Gateway A

🔀 Redis pub/sub

🛰️ Gateway B

📥 Subscriber

Self-healing on Redis outage

The fan-out adapter (RedisPubSubAdapter) survives a dropped Redis connection without operator intervention:

  • Bounded exponential backoff — reconnects starting at 1s, doubling on each failed attempt, capped at 30s. It never gives up unless the adapter is disconnected.
  • Automatic re-subscription — every channel is re-subscribed on the fresh pub/sub handle after reconnect, so operators do not re-subscribe.
  • Degraded surfacing — records itself as route:redis-pubsub in the degraded registry with a redacted reason and a retry_hint, so gateway status / gateway doctor / health()["degraded_owners"] show the outage.
  • Dropped-write counting — publish, set_presence, remove_presence, store_message, and delete_message calls made while disconnected are counted (previously silent no-ops) and exposed as dropped_writes.
  • Clears on recovery — the degraded record is removed and delivery resumes once Redis is reachable again.
Diagram
DegradedCapabilityRegistryRedisListenerDegradedCapabilityRegistryRedisListenerloop[reconnect (1s → 30s cap)]get_message()connection droppedmark(route:redis-pubsub, state=stale)reconnect attemptreconnectedre-subscribe every channelclear(route, redis-pubsub)

What operators see

During an outage, gateway status reports the degraded transport under push and lists the owner in degraded_owners:

json
{
  "push": {
    "redis_connected": false,
    "redis_degraded": true,
    "redis_dropped_writes": 3,
    "online_clients": 12
  },
  "degraded_owners": [
    {
      "owner_kind": "route",
      "owner_id": "redis-pubsub",
      "state": "stale",
      "reason": "redis pub/sub disconnected: Connection refused",
      "retry_hint": "check Redis connectivity; see `praisonai gateway doctor`"
    }
  ]
}

After recovery, the three redis_* fields return to healthy and the route:redis-pubsub entry is gone:

json
{
  "push": {
    "redis_connected": true,
    "redis_degraded": false,
    "redis_dropped_writes": 3,
    "online_clients": 12
  },
  "degraded_owners": []
}

The reason only ever contains the exception message — the Redis connection URL and password are never surfaced. The log line to watch for on recovery is Redis push adapter reconnected (server_id=<uuid>).


Configuration Options

PushConfig

OptionTypeDefaultDescription
enabledboolFalseFeature toggle
redisRedisConfigNoneCross-server scaling
presencePresenceConfigPresenceConfig()Online/offline tracking
deliveryDeliveryConfigDeliveryConfig()ACK and retry settings
pollingPollingConfigPollingConfig()Long-poll fallback

DeliveryConfig

OptionTypeDefaultDescription
enabledboolTrueDelivery guarantees
ack_timeoutint30Seconds to wait for ACK
max_retriesint3Retry attempts
store_backendstr"memory""memory" or "redis"

PollingConfig

OptionTypeDefaultDescription
enabledboolTrueToggle polling fallback
long_poll_timeoutint30Long-poll hang duration (seconds)
max_batch_sizeint100Max messages per poll response
max_queue_sizeint1000Per-client in-memory queue bound. >0 overflows to the durable store on full; 0 keeps the queue unbounded (no overflow). See Durable Polling.

Best Practices

Enable Redis for multi-server

Without Redis, channels exist on one gateway instance only.

Keep push opt-in

PushConfig(enabled=False) adds zero overhead — enable only when clients subscribe.

Use polling fallback on corporate networks

Set fallback_to_polling=True on PushClient when WebSocket is blocked — the client will still receive messages, but publish / create-channel / presence-query become unavailable. Check client.supports_publish before producer calls.

Separate from A2A webhooks

Real-time channels differ from A2A task webhooks — use the right page for your pattern.

Watch redis_dropped_writes in HA deployments

A non-zero push.redis_dropped_writes after an outage tells you how many events fanned out one-sided. Alert on it if replay matters — those writes are gone from the cross-instance transport’s perspective.

Alert on route:redis-pubsub degradation

The degraded_owners entry for route:redis-pubsub is the single place to hook a pager for cross-instance-transport outages. It appears for the duration of the outage and clears automatically on reconnect.


Related