# SpacetimeDB Subscriptions and Real-Time Updates: A Complete Technical Guide

> Master SpacetimeDB subscriptions and real-time updates with this technical guide. Learn how SpacetimeDB uses WebSockets and a three-layer architecture for efficient data delivery.

- Repository: [Clockwork Labs/SpacetimeDB](https://github.com/clockworklabs/SpacetimeDB)
- Tags: deep-dive
- Published: 2026-03-09

---

**SpacetimeDB delivers real-time data to clients through a subscription mechanism built on top of WebSocket connections, using a three-layer architecture involving client-side builders, WebSocket transport, and server-side routing.**

The `clockworklabs/SpacetimeDB` repository implements this system as a tightly integrated stack that enables clients to subscribe to SQL queries and receive live updates as the underlying data changes. Understanding how SpacetimeDB subscriptions and real-time updates work requires examining the interaction between the SDK, the WebSocket transport layer, and the server-side routing logic.

## How SpacetimeDB Subscriptions Work: The Three-Layer Architecture

The subscription system is divided into three logical layers, each with distinct responsibilities and key types.

### Client-Side API Layer

The client-side API is responsible for building subscriptions, registering callbacks, and driving the WebSocket driver. The primary types include `SubscriptionBuilder`, `SubscriptionHandle`, and `DbConnection`. This logic resides in [`sdks/rust/src/subscription.rs`](https://github.com/clockworklabs/SpacetimeDB/blob/main/sdks/rust/src/subscription.rs), where the builder pattern allows developers to chain configuration methods before finalizing the subscription.

### WebSocket Transport Layer

This layer handles the HTTP to WebSocket upgrade, maintains connection keep-alives through ping/pong frames, serializes and deserializes messages, and enforces protocol versions. Key types include `WebSocketUpgrade`, `WebSocketResponse`, and `WebSocketStream`. The implementation is located in [`crates/client-api/src/util/websocket.rs`](https://github.com/clockworklabs/SpacetimeDB/blob/main/crates/client-api/src/util/websocket.rs).

### Server-Side Routing Layer

The server-side routing accepts the WebSocket upgrade, creates a `ClientConnection`, injects the initial identity token, and forwards subscription requests to the module host. The primary entry point is the `handle_websocket` function in [`crates/client-api/src/routes/subscribe.rs`](https://github.com/clockworklabs/SpacetimeDB/blob/main/crates/client-api/src/routes/subscribe.rs), which utilizes `ClientConnection` and `WsVersion` types to manage the connection lifecycle.

## Building a Subscription with the SpacetimeDB SDK

To create a subscription, you use the `subscription_builder` method on a connection object, register callbacks, and specify the SQL query to monitor.

```rust
use spacetimedb::Client;
use spacetimedb::module::MyModule;

#[tokio::main]
async fn main() -> spacetimedb::Result<()> {
    // Connect to the remote module (WebSocket opens lazily)
    let conn = MyModule::connect("ws://localhost:8080").await?;

    // Create a subscription builder and configure callbacks
    let sub_handle = conn
        .subscription_builder()
        .on_applied(|ctx| {
            println!("Subscription applied for query {}", ctx.query_set_id());
        })
        .on_error(|err_ctx, err| {
            eprintln!("Subscription error: {err}");
        })
        // Register the SQL query
        .subscribe("SELECT id, value FROM my_table WHERE value > 10");

    // Run the event loop that processes WebSocket messages
    conn.run_async().await?;
    Ok(())
}

```

The **builder pattern** returns a `SubscriptionBuilder` that allows method chaining for configuration. The `on_applied` callback fires when the server acknowledges the subscription, while `on_error` handles query rejection or schema changes. Internally, the `subscribe` call creates a `SubscriptionHandleImpl`, registers it with a local `SubscriptionManager`, and queues a `PendingMutation::Subscribe` for transmission by the driver.

## WebSocket Transport: Upgrades, Keep-Alives, and Message Ordering

The HTTP `GET /subscribe/:name_or_identity` endpoint is handled by `handle_websocket` in [`crates/client-api/src/routes/subscribe.rs`](https://github.com/clockworklabs/SpacetimeDB/blob/main/crates/client-api/src/routes/subscribe.rs). The process involves three critical steps:

1. **Protocol negotiation**: The server selects between `ws_v1::BIN_PROTOCOL`, `ws_v1::TEXT_PROTOCOL`, or `ws_v2::BIN_PROTOCOL`. This selection determines the wire format (`Protocol::Binary` or `Protocol::Text`) and the version (`WsVersion::V1` or `WsVersion::V2`).

2. **WebSocket upgrade**: The `WebSocketUpgrade::select_protocol` function constructs a `WebSocketResponse` containing the mandatory `Sec-WebSocket-Accept` header and optional `Sec-WebSocket-Protocol` headers.

3. **Actor spawn**: `tokio::spawn` launches `ws_client_actor`, which owns three concurrent loops:
   - **`ws_recv_loop`**: Reads incoming `WsMessage`s, updates the idle timer, and forwards parsed `ClientMessage`s to the message handler.
   - **`ws_send_loop`**: Drains a bounded outgoing queue, interleaving control frames (`Ping`, `Close`) with subscription updates.
   - **`ws_idle_timer`**: Resets the idle deadline when messages are received; closes the connection if the deadline expires.

The **ping/pong** flow ensures that idle connections are terminated quickly while keeping bandwidth-constrained clients alive. Because the WebSocket driver runs on a **single `tokio` task**, all messages are strictly ordered per client, guaranteeing deterministic application of updates.

## Server-Side Subscription Lifecycle

When the client sends a `Subscribe` message, the following lifecycle events occur:

| Event | Server Processing | Client Reaction |
|-------|-------------------|---------------|
| **Subscribe → Server** | The `ClientConnection` queues the subscription request, then the module host receives it via `module_rx`. | The driver marks the subscription as *Sent*; no immediate action. |
| **Server → Client: SubscribeApplied** | The module host confirms the query set ID. | The client's `SubscriptionManager::subscription_applied` invokes the stored `on_applied` callback. The client now receives real-time updates for that query. |
| **Server → Client: UpdateRows** | Each data change is encoded as `ws_v2::ServerMessage::TableRows` (or V1 equivalent) inside `ws_send_loop`. The message is serialized with `ws_encode_message`. | `ws_client_message_handler` extracts `DataMessage`s. `ClientConnection::handle_message` deserializes the rows and updates the local cache. |
| **Unsubscribe** | When `SubscriptionHandle::unsubscribe_then` is called, `SubscriptionState::unsubscribe_then` queues a `PendingMutation::Unsubscribe`. The server acknowledges with `UnsubscribeApplied`. | `SubscriptionManager::unsubscribe_applied` invokes the optional `on_ended` callback. The client stops receiving updates for that query. |

The **`SubscriptionManager`** (located in [`sdks/rust/src/subscription.rs`](https://github.com/clockworklabs/SpacetimeDB/blob/main/sdks/rust/src/subscription.rs)) serves as the authoritative registry of all active subscriptions. It tracks the subscription's server state (`Pending`, `Sent`, `Applied`, `Ended`, `Error`), manages callbacks (`on_applied`, `on_error`, `on_ended`), and handles race conditions such as unsubscribing before the subscription is sent (see `PendingUnsubscribeResult`).

All subscription state lives on the client; the server only knows the query set ID and the fact that a client is subscribed. This design enables **stateless reconnections**—after a network drop, the client can re-issue its pending subscriptions without losing data.

## Real-Time Update Flow: End-to-End

The following diagram illustrates the complete flow from client subscription to server-side data updates:

```

+-------------------+         +----------------------+         +-------------------+
|  Client SDK       |   WS    |  SpacetimeDB Server  |   WS    |  Module Host      |
| (SubscriptionBuilder) | <----> | (handle_websocket)  | <----> | (apply_query)     |
+-------------------+         +----------------------+         +-------------------+

1. SDK builds a Subscribe request
2. handle_websocket creates a ClientConnection
3. ClientConnection queues PendingMutation::Subscribe
4. ws_send_loop serializes to ws_v2::Subscribe -> server
5. Module host registers the query set, sends SubscribeApplied
6. SubscriptionManager fires on_applied
7. Subsequent DB changes -> ws_encode_message -> DataMessage frames
8. ws_recv_loop -> ClientConnection::handle_message -> local cache
9. Optional on_error / on_ended callbacks fire on failures or unsubscription

```

Because the WebSocket driver runs on a **single `tokio` task**, all messages (subscription control, data updates, pings) are strictly ordered per client, guaranteeing deterministic application of updates.

## Practical Patterns for SpacetimeDB Subscriptions

The following patterns demonstrate common use cases for working with subscriptions in production applications:

| Use Case | Code Pattern |
|----------|--------------|
| **Subscribe to a single query** | `conn.subscription_builder().subscribe("SELECT * FROM scores")` |
| **Subscribe to multiple queries** | `builder.add_query(...).add_query(...).subscribe()` (see `TypedSubscriptionBuilder` lines 30-46) |
| **Subscribe to all tables (debug/quick-start)** | `builder.subscribe_to_all_tables()` (lines 12-18) |
| **React to data updates** | The SDK automatically updates the generated `RemoteTables` view; read the latest rows after `frame_tick()` or inside a custom `on_applied`/`on_error` callback. |
| **Graceful shutdown** | Call `sub_handle.unsubscribe_then(|ctx| println!("gone"))` or simply drop the `DbConnection` (disconnect triggers `on_disconnect`). |

## Key Source Files and Implementation Details

Understanding the subscription architecture requires familiarity with the following source files:

| File | Implementation Details |
|------|------------------------|
| **[`crates/client-api/src/routes/subscribe.rs`](https://github.com/clockworklabs/SpacetimeDB/blob/main/crates/client-api/src/routes/subscribe.rs)** | WebSocket upgrade handling, `ClientConnection` creation, initial identity token injection (lines 12-31). |
| **[`crates/client-api/src/util/websocket.rs`](https://github.com/clockworklabs/SpacetimeDB/blob/main/crates/client-api/src/util/websocket.rs)** | Low-level WebSocket handshake, protocol selection, ping/pong handling (lines 1-80). |
| **[`sdks/rust/src/subscription.rs`](https://github.com/clockworklabs/SpacetimeDB/blob/main/sdks/rust/src/subscription.rs)** | `SubscriptionBuilder`, `SubscriptionManager`, state machine implementation, callback registration (lines 46-180). |
| **[`sdks/rust/src/subscription.rs`](https://github.com/clockworklabs/SpacetimeDB/blob/main/sdks/rust/src/subscription.rs)** (continued) | `SubscriptionHandle`, `TypedSubscriptionBuilder`, `IntoQueries` helper traits (lines 180-300). |
| **`templates/*/src/module_bindings/mod.rs`** | Auto-generated bindings exposing `subscription_builder()` to end-users (lines 44-49). |
| **[`sdks/rust/tests/view-client/src/module_bindings/mod.rs`](https://github.com/clockworklabs/SpacetimeDB/blob/main/sdks/rust/tests/view-client/src/module_bindings/mod.rs)** | Example usage in the test suite (see `subscription_error_smoke_test`). |
| **[`crates/client-api/src/lib.rs`](https://github.com/clockworklabs/SpacetimeDB/blob/main/crates/client-api/src/lib.rs)** | Public re-exports including `ClientConnection` and `SubscriptionBuilder`. |

## Summary

- SpacetimeDB implements real-time data delivery through a **subscription mechanism** built on WebSocket connections, utilizing a three-layer architecture separating client API, transport, and server routing concerns.
- The **client-side SDK** provides a builder pattern via `SubscriptionBuilder` in [`sdks/rust/src/subscription.rs`](https://github.com/clockworklabs/SpacetimeDB/blob/main/sdks/rust/src/subscription.rs), allowing developers to register callbacks for subscription lifecycle events (`on_applied`, `on_error`, `on_ended`).
- The **WebSocket transport layer** in [`crates/client-api/src/util/websocket.rs`](https://github.com/clockworklabs/SpacetimeDB/blob/main/crates/client-api/src/util/websocket.rs) handles protocol negotiation (V1/V2), connection keep-alives via ping/pong, and strict message ordering through a single `tokio` task per client.
- The **server-side routing** in [`crates/client-api/src/routes/subscribe.rs`](https://github.com/clockworklabs/SpacetimeDB/blob/main/crates/client-api/src/routes/subscribe.rs) manages the `ClientConnection` lifecycle, forwarding subscription requests to the module host and streaming row updates back to clients.
- All subscription state resides on the client, enabling **stateless reconnections** where pending subscriptions can be re-issued after network interruptions without data loss.

## Frequently Asked Questions

### How does SpacetimeDB handle subscription reconnections after network failures?

Because all subscription state lives on the client side, SpacetimeDB enables stateless reconnections. When a network drop occurs, the client can re-establish the WebSocket connection and re-issue its pending subscriptions without losing data. The `SubscriptionManager` in [`sdks/rust/src/subscription.rs`](https://github.com/clockworklabs/SpacetimeDB/blob/main/sdks/rust/src/subscription.rs) tracks the server state (`Pending`, `Sent`, `Applied`) and handles race conditions such as unsubscribing before the subscription is fully sent.

### What is the difference between WebSocket protocol V1 and V2 in SpacetimeDB?

SpacetimeDB supports two WebSocket protocol versions negotiated during the handshake in [`crates/client-api/src/util/websocket.rs`](https://github.com/clockworklabs/SpacetimeDB/blob/main/crates/client-api/src/util/websocket.rs). **V1** supports both binary (`ws_v1::BIN_PROTOCOL`) and text (`ws_v1::TEXT_PROTOCOL`) formats, while **V2** (`ws_v2::BIN_PROTOCOL`) uses an optimized binary format for `ServerMessage::TableRows` and other subscription updates. The selected protocol determines the serialization method used by `ws_encode_message` in the transport layer.

### How are subscription queries validated on the server?

When a client sends a `Subscribe` message, the server-side `handle_websocket` function in [`crates/client-api/src/routes/subscribe.rs`](https://github.com/clockworklabs/SpacetimeDB/blob/main/crates/client-api/src/routes/subscribe.rs) creates a `ClientConnection` that queues the subscription request. The module host receives this via `module_rx` and validates the SQL query syntax and permissions. If validation fails, the server sends an error message that triggers the client's `on_error` callback via `SubscriptionManager::subscription_error`.

### Can I subscribe to multiple tables with a single subscription handle?

Yes, the SDK supports subscribing to multiple queries through a single builder. Using `TypedSubscriptionBuilder` in [`sdks/rust/src/subscription.rs`](https://github.com/clockworklabs/SpacetimeDB/blob/main/sdks/rust/src/subscription.rs), you can chain multiple `add_query()` calls before invoking `subscribe()`. Alternatively, for development and debugging purposes, you can use `subscribe_to_all_tables()` to automatically generate subscriptions for every table in the module. All queries registered to a single builder share the same lifecycle callbacks (`on_applied`, `on_error`, `on_ended`).