Sockudo
Realtime Clients

Python

Use the async sockudo-python realtime client with Protocol V2, protected channels, recovery, filters, deltas, history, and encrypted payloads.

sockudo-python is the official asynchronous realtime client for Python. It is intended for long-running services, agents, workers, CLIs, and other asyncio applications that need a WebSocket connection to Sockudo. It is different from sockudo-http-python, which is the trusted server-side HTTP SDK used to publish and sign authorization responses.

The client requires Python 3.10 or newer and uses Protocol V2 by default.

Install

python -m pip install sockudo-python

For development from this monorepo:

python -m pip install -e client-sdks/sockudo-python

Connect and subscribe

Create one client per application process, bind handlers before connecting, and close it during application shutdown:

import asyncio

from sockudo_python import SockudoClient, SockudoOptions


async def main() -> None:
    client = SockudoClient(
        "app-key",
        SockudoOptions(
            cluster="local",
            ws_host="127.0.0.1",
            ws_port=6001,
            force_tls=False,
            protocol_version=2,
            connection_recovery=True,
        ),
    )

    channel = client.subscribe("public-updates")
    channel.bind("price-updated", lambda payload, meta: print(payload))

    try:
        await client.connect()
        await asyncio.Event().wait()
    finally:
        await client.disconnect()


asyncio.run(main())

connect() returns after starting the connection; event callbacks run on the same asyncio event loop. Do not perform blocking file, database, or network work inside a callback. Schedule or await that work in an async task instead.

Self-hosted connection options

OptionPurposeProduction guidance
ws_hostWebSocket host without a schemeUse the public load balancer or ingress hostname
ws_port / wss_portPlaintext and TLS WebSocket portsUsually 80 and 443 behind a proxy
force_tlsSelect wss:// when trueEnable outside local development
protocol_version2 for Sockudo-native features; 1 for Pusher compatibilityPrefer 2 for new applications
connection_recoveryResume from the last stream_id and serialEnable when missed events matter
max_reconnect_attemptsRetry limit; None means unlimitedDefaults to 6
max_reconnect_gap_in_secondsMaximum quadratic backoff delayDefaults to 120.0 seconds
wire_formatJSON, MessagePack, or ProtobufUse a binary format only when every endpoint supports it

cluster remains required by the options type, but a custom ws_host is the important routing setting for self-hosted deployments.

Lifecycle and cleanup

Bind connection events to make health and reconnect behavior visible:

def on_state_change(change, _) -> None:
    print(f"connection: {change['previous']} -> {change['current']}")


client.bind("state_change", on_state_change)
client.bind(
    "connected",
    lambda data, _: print("socket id:", data.get("socket_id")),
)
client.bind("connecting", lambda *_: print("connecting"))
client.bind("reconnecting", lambda *_: print("reconnecting"))
client.bind("error", lambda error, _: print("error:", error))

Remove a subscription when its consumer goes away. This also clears the channel's recovery and delta state:

await client.unsubscribe("public-updates")

await client.close() is an alias for disconnect(). A manual disconnect stops reconnect timers; an unexpected transport failure uses the client's reconnect policy. Retries emit reconnecting and use quadratic backoff (0s, 1s, 4s, 9s) capped by max_reconnect_gap_in_seconds; protocol retry and TLS-upgrade close codes remain immediate.

Private and presence authorization

The client sends its socket_id and channel name to your backend. Your backend must authenticate the user, enforce access to the requested channel, and use a server SDK to sign the response. Never put the app secret in Python client code.

from sockudo_python import ChannelAuthorizationOptions

client = SockudoClient(
    "app-key",
    SockudoOptions(
        cluster="local",
        ws_host="realtime.example.com",
        force_tls=True,
        channel_authorization=ChannelAuthorizationOptions(
            endpoint="https://api.example.com/sockudo/auth",
            headers={"X-Client": "worker"},
        ),
    ),
)

private_orders = client.subscribe("private-orders")
private_orders.bind("order-placed", lambda data, _: print(data))

Presence subscriptions receive the initial member set and subsequent joins and leaves:

presence = client.subscribe("presence-lobby")
presence.bind(
    "sockudo:subscription_succeeded",
    lambda members, _: print("members:", members),
)
presence.bind("sockudo:member_added", lambda member, _: print("joined:", member))
presence.bind("sockudo:member_removed", lambda member, _: print("left:", member))

await client.connect()
await presence.update({"status": "editing"})

The update() call is a Protocol V2 presence update. It changes member data without a leave and rejoin cycle.

Capability-token authentication

Protocol V2 can authorize the WebSocket itself with a scoped capability token. Use an async callback so the client can obtain fresh credentials:

from sockudo_python import TokenAuthData


async def fetch_token() -> TokenAuthData:
    response = await call_your_backend()
    return TokenAuthData(
        token=response["token"],
        expires_in=response["expires_in"],
    )


client = SockudoClient(
    "app-key",
    SockudoOptions(
        cluster="local",
        ws_host="realtime.example.com",
        force_tls=True,
        auth_callback=fetch_token,
    ),
)

Expiry metadata lets the SDK schedule a refresh before expiration. Opaque callback tokens without expiry metadata are refreshed after code 40142 and before reconnects. Static and revoked tokens are never resent. Token auth is rejected outside Protocol V2.

Filters, event selection, and delta compression

Protocol V2 subscriptions can combine event names, tag filters, and a bounded expression. All supplied conditions must match:

from sockudo_python import (
    ChannelDeltaSettings,
    DeltaAlgorithm,
    Filter,
    SubscriptionOptions,
)

market = client.subscribe(
    "price:btc",
    options=SubscriptionOptions(
        events=["price.updated"],
        filter=Filter.and_(
            Filter.eq("market", "spot"),
            Filter.gt("spread", "0"),
        ),
        expression="data.price >= `100`",
        delta=ChannelDeltaSettings(
            enabled=True,
            algorithm=DeltaAlgorithm.XDELTA3,
        ),
    ),
)

Use delta compression for frequently changing, structurally similar payloads such as order books. The SDK reconstructs full payloads before invoking your handler. Monitor client.get_delta_stats() and fall back to full events if base continuity is lost. See Filters and delta compression.

Recovery, rewind, and late joins

Recovery resumes a previously attached channel after a connection interruption. Rewind asks the server for a bounded amount of earlier history at subscription time:

from sockudo_python import SubscriptionOptions, SubscriptionRewind

market = client.subscribe(
    "market:btc",
    options=SubscriptionOptions(
        rewind=SubscriptionRewind.seconds_back(30),
    ),
)

client.bind("sockudo:resume_success", lambda data, _: print(data))
client.bind("sockudo:resume_failed", lambda data, _: print(data))
market.bind("sockudo:rewind_complete", lambda data, _: print(data))

Treat resume_failed as a continuity break: clear derived local state and fetch an authoritative snapshot before applying new events.

For gap-free late joins, proxy channel history through your backend and request history only up to the subscription's attach point:

from sockudo_python import ChannelHistoryOptions, ChannelHistoryParams

client = SockudoClient(
    "app-key",
    SockudoOptions(
        cluster="local",
        channel_history=ChannelHistoryOptions(
            endpoint="https://api.example.com/sockudo/channel-history",
        ),
    ),
)

channel = client.subscribe("orders")
page = await channel.history(
    ChannelHistoryParams(limit=50, until_attach=True),
)

The proxy owns Sockudo credentials and validates which channels the requesting user may read.

Presence history and snapshots

Presence history is also proxy-backed. The endpoint receives a channel, action, and parameters, then calls the signed server REST API:

from sockudo_python.client import (
    PresenceHistoryOptions,
    PresenceHistoryParams,
    PresenceSnapshotParams,
)

client = SockudoClient(
    "app-key",
    SockudoOptions(
        cluster="local",
        presence_history=PresenceHistoryOptions(
            endpoint="https://api.example.com/sockudo/presence-history",
        ),
    ),
)

presence = client.subscribe("presence-lobby")
page = await presence.history(
    PresenceHistoryParams(limit=50, direction="newest_first"),
)
if page.has_next():
    next_page = await page.next()

snapshot = await presence.snapshot(PresenceSnapshotParams(at_serial=4))

Use current membership for live UI, history for audit or timelines, and a snapshot to reconstruct membership at a specific serial. See Presence history.

Versioned messages

Create and mutate versioned messages through a trusted proxy. These helpers do not expose app credentials or send mutation frames over the WebSocket:

from sockudo_python import VersionedMessageOptions

client = SockudoClient(
    "app-key",
    SockudoOptions(
        cluster="local",
        versioned_messages=VersionedMessageOptions(
            endpoint="https://api.example.com/sockudo/versioned-messages",
        ),
    ),
)

ack = await client.versioned_messages.create(
    "chat:room-1",
    "message.created",
    {"text": "hello"},
)
if ack.message_id is None:
    raise RuntimeError("Sockudo did not return a message_id")

await client.versioned_messages.append(
    "chat:room-1",
    ack.message_id,
    {"text": " world"},
)
await client.versioned_messages.update(
    "chat:room-1",
    ack.message_id,
    {"text": "hello world"},
)

Apply mutations in serial order. If an append arrives before its base, fetch the latest visible message from the proxy before applying later appends.

Encrypted channels

private-encrypted-* subscriptions are decrypted automatically. The authorization response must include a shared_secret derived by your trusted backend:

encrypted = client.subscribe("private-encrypted-documents")
encrypted.bind("doc-updated", lambda data, _: print(data))

Encryption protects event content end to end. It does not replace TLS, authorization, channel naming policy, or secret rotation.

User sign-in

Configure a user-auth endpoint when using user-targeted events or watchlists:

from sockudo_python import UserAuthenticationOptions

client = SockudoClient(
    "app-key",
    SockudoOptions(
        cluster="local",
        user_authentication=UserAuthenticationOptions(
            endpoint="https://api.example.com/sockudo/user-auth",
        ),
    ),
)

await client.connect()
await client.user.sign_in()

The backend must bind the authenticated application user to the current socket_id; never accept a user ID supplied by the client without checking the session.

Error handling and production checklist

  • Catch SockudoException around explicit connect, auth, proxy, and mutation operations; connection errors are also emitted through the connection.
  • Put timeouts and authentication on every proxy endpoint.
  • Use TLS and a public load-balancer hostname outside local development.
  • Keep callbacks fast and move blocking work off the asyncio event loop.
  • Make handlers idempotent because reconnect and application retries can repeat work even when transport deduplication is enabled.
  • On a failed resume or unrecoverable delta, reload authoritative state.
  • Unsubscribe unused channels and disconnect cleanly during process shutdown.
  • Use the HTTP Python server SDK for publishing, signing auth, webhooks, history administration, and push.

On this page