Skip to content
Blog / One WebSocket, many subscriptions: smarter Realtime in Appwrite
9 min

One WebSocket, many subscriptions: smarter Realtime in Appwrite

Appwrite Realtime now keeps one persistent WebSocket and drives subscriptions with messages instead of cramming state into the connection URL. Client SDKs expose unsubscribe(), update(), and disconnect() for clearer lifecycle control.

One WebSocket, many subscriptions: smarter Realtime in Appwrite

Realtime features are where users feel your app is “alive”: collaborative edits, live dashboards, and instant feedback when data changes. That experience depends on how predictable your subscription lifecycle is. If every tweak to what you listen for forces a full reconnect, you pay in latency, battery, and mental overhead.

Appwrite Realtime now uses a message-based protocol on a single persistent WebSocket. The service applies subscription changes incrementally over the socket instead of treating the WebSocket URL as the source of truth for what you listen to.

Previously, subscription details were largely carried in the query string of the Realtime WebSocket URL. That tied you to URL length limits enforced by browsers, servers, and proxies, so in practice you could only combine so many channels and so much metadata before the connection string itself became a bottleneck.

That friction grew once we shipped Realtime queries to filter subscription events on the server, and larger query payloads made the URL ceiling easier to hit. Channels and queries are now sent over the established socket, so you are not capped by query-string size when scaling up listeners or filters.

One connection, many subscriptions

You create a Realtime instance from your configured Client, then call subscribe for each logical listener. The example below subscribes to two channels (files and account) on a single connection, shown across Appwrite clients in the tabs below.

import { Client, Realtime, Channel } from "appwrite";

const client = new Client()
    .setEndpoint('https://<REGION>.cloud.appwrite.io/v1')
    .setProject('<PROJECT_ID>');

const realtime = new Realtime(client);

const sub1 = await realtime.subscribe(Channel.files(), response => {
    console.log(response);
});

const sub2 = await realtime.subscribe(Channel.account(), response => {
    console.log(response);
});

Unsubscribing one handle does not drop unrelated listeners: the Realtime service keeps the shared connection and removes only what you asked for.

Call unsubscribe() on a subscription handle to stop that listener, and realtime.disconnect() to close the socket entirely. The legacy close() alias remains for backwards compatibility. See the subscribe documentation for the full API.

await sub1.unsubscribe(); // only sub1 stops receiving events
await sub2.unsubscribe(); // only sub2

// When your UI is done with Realtime (for example on unmount):
realtime.disconnect();

In-place subscription updates

Changing channels or queries no longer requires recreating the subscription. Call update() on the subscription handle to adjust the channels or queries while the WebSocket stays open. The API is available across Web, Flutter, Apple, and Android SDKs (see Subscribe).

import { Client, Realtime, Channel, Query } from "appwrite";

const client = new Client()
    .setEndpoint('https://<REGION>.cloud.appwrite.io/v1')
    .setProject('<PROJECT_ID>');

const realtime = new Realtime(client);

const subscription = await realtime.subscribe(Channel.files(), response => {
    console.log(response);
});

await subscription.update({
    channels: [Channel.tablesdb('<DATABASE_ID>').table('<TABLE_ID>').row('<ROW_ID>')],
    queries: [Query.equal('title', ['todo'])],
});

Why this matters

  • Clearer ownership: Each subscription is its own handle with a predictable lifecycle.
  • Better performance: Fewer full reconnects when your app state shifts.
  • Simpler UI code: Mount paths call subscribe (or update), unmount paths call unsubscribe or disconnect.

More resources

Start building with Appwrite today