--- id: 'protocol' title: 'Realtime Protocol' description: 'Understanding Realtime Protocol' --- ## WebSocket connection setup To start the connection we use the WebSocket URL, which for: - Supabase projects: `wss://.supabase.co/realtime/v1/websocket?apikey=` - self-hosted projects: `wss://:/socket/websocket?apikey=` As an example, using [websocat](https://github.com/vi/websocat), you would run the following command in your terminal: ```bash # With Supabase websocat "wss://.supabase.co/realtime/v1/websocket?apikey=" # With self-hosted websocat "wss://:/socket/websocket?apikey=" ``` During this stage you can also set other URL params: - `vsn`: sets the protocol version. Possible values are `1.0.0` and `2.0.0`. Defaults to `1.0.0`. - `log_level`: sets the log level to be used by this connection to help you debug potential issues. This only affects server side logs. After connecting a `phx_join` event must be sent to the server to join a channel. The next sections outline the different messages types and events that are supported. ## Protocol messages Messages can be serialized in different formats. The Realtime protocol supports two versions: `1.0.0` and `2.0.0`. ## 1.0.0 Version 1.0.0 is minimal. It uses JSON as the serialization format for messages. The underlying WebSocket messages are all text frames. Messages contain the following fields: - `event`: The type of event being sent or received. Example `phx_join`, `postgres_changes`, `broadcast`, etc. - `topic`: The topic to which the message belongs. This is a string that identifies the channel or context of the message. - `payload`: The data associated with the event. This can be any JSON-serializable data structure, such as an object or an array. - `ref`: A unique reference ID for the message. This is useful to track replies to a specific message. - `join_ref`: A unique reference ID to uniquely identify a joined topic for pushes, broadcasts, replies, etc. Example: ```json { "topic": "realtime:presence-room", "event": "phx_join", "payload": { "config": { "broadcast": { "ack": false, "self": false }, "presence": { "enabled": false }, "private": false } }, "ref": "1", "join_ref": "1" } ``` ## 2.0.0 Version 2.0.0 uses text and binary WebSocket frames. ### Text frames Text frames are always JSON encoded, but unlike version 1.0.0, they use a JSON array where the element order must be exactly: - `join_ref` - `ref` - `topic` - `event` - `payload` Example: ```json [ "1", "1", "realtime:presence-room", "phx_join", { "config": { "broadcast": { "ack": false, "self": false }, "presence": { "enabled": false }, "private": false } } ] ``` ### Binary frames The two special message types have a well defined binary format where the first byte defines the type of message. Both are used to send and receive broadcast events. See the [client](#client-sent-events) and [server](#server-sent-events) sent events for more details. | Code | Type | Description | | ---- | ------------------- | ----------------------------- | | 3 | USER_BROADCAST_PUSH | User-initiated broadcast push | | 4 | USER_BROADCAST | User broadcast message | #### User Broadcast Push ``` 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Type (0x03) | Join Ref Size | Ref Size | Topic Size | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |User Event Size| Metadata Size | Payload Enc. | Join Ref ... | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Ref (variable length) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Topic (variable length) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | User Event (variable length) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Metadata (variable length) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | User Payload (variable length) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ``` **Field Descriptions:** - **Type**: 1 byte, value = 0x03 - **Join Ref Size**: 1 byte, size of join reference string (max 255) - **Ref Size**: 1 byte, size of reference string (max 255) - **Topic Size**: 1 byte, size of topic string (max 255) - **User Event Size**: 1 byte, size of user event string (max 255) - **Metadata Size**: 1 byte, size of metadata string (max 255) - **Payload Encoding**: 1 byte (0 = binary, 1 = JSON) - **Join Ref**: Variable length string - **Ref**: Variable length string - **Topic**: Variable length string - **User Event**: Variable length string - **Metadata**: Variable length JSON string - **User Payload**: Variable length payload data #### User Broadcast ``` 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Type (0x04) | Topic Size |User Event Size| Metadata Size | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Payload Enc. | Topic (variable length) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | User Event (variable length) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Metadata (variable length) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | User Payload (variable length) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ``` **Field Descriptions:** - **Type**: 1 byte, value = 0x04 - **Topic Size**: 1 byte, size of topic string (max 255) - **User Event Size**: 1 byte, size of user event string (max 255) - **Metadata Size**: 1 byte, size of metadata JSON string (max 255) - **Payload Encoding**: 1 byte (0 = binary, 1 = JSON) - **Topic**: Variable length string - **User Event**: Variable length string - **Metadata**: Variable length JSON string - **User Payload**: Variable length payload data ## Event types Messages for all events are encoded as text frames using JSON except with the `broadcast` event type which can happen on both text and binary frames. ### Client sent events | Event Type | Description | Requires Ref | Requires Join Ref | | -------------- | -------------------------------------------------------- | ------------ | ----------------- | | `phx_join` | Initial message to join a channel and configure features | ✅ | ✅ | | `phx_leave` | Message to leave a channel | ✅ | ✅ | | `heartbeat` | Heartbeat message to keep the connection alive | ✅ | ⛔ | | `access_token` | Message to update the access token | ✅ | ✅ | | `broadcast` | Broadcast message sent to all clients in a channel | ✅ | ✅ | | `presence` | Presence state update sent after joining a channel | ✅ | ✅ | #### phx_join This is the initial message required to join a channel. The client sends this message to the server to join a specific topic and configure the features it wants to use, such as Postgres changes, Presence, and Broadcast. The payload of the `phx_join` event contains the configuration options for the channel. ```ts { "config": { "broadcast": { "ack": boolean, "self": boolean, "replay" : { "since": integer, "limit": integer }, "replication_ready": boolean }, "presence": { "enabled": boolean, "key": string }, "postgres_changes": [ { "event": string, "schema": string, "table": string, "filter": string, "select": string[] } ] "private": boolean }, "access_token": string } ``` - `config`: - `private`: Whether the channel is private - `broadcast`: Configuration options for broadcasting messages - `ack`: Acknowledge broadcast messages - `self`: Include the sender in broadcast messages - `replay`: Configuration options for broadcast replay (Optional) - `since`: Replay messages since a specific timestamp in milliseconds - `limit`: Limit the number of replayed messages (Optional) - `replication_ready`: When `true`, the server emits a `system` event once the Postgres replication connection backing this channel is established and ready to stream changes (Optional). See the [system](#system) event for the payload shape. - `presence`: Configuration options for presence tracking - `enabled`: Whether presence tracking is enabled for this channel - `key`: Key to be used for presence tracking, if not specified or empty, a UUID will be generated and used - `postgres_changes`: Array of configurations for Postgres changes - `event`: Database change event to listen to, accepts `INSERT`, `UPDATE`, `DELETE`, or `*` to listen to all events. - `schema`: Schema of the table to listen to, accepts `*` wildcard to listen to all schemas - `table`: Table of the database to listen to, accepts `*` wildcard to listen to all tables - `filter`: Filter to be used when pulling changes from the database. A filter is a `column=operator.value` expression (for example `id=eq.1` or `title=like.%foo%`). Multiple conditions can be combined with commas and are applied as an `AND` (for example `id=gt.0,id=lt.100`). Any operator can be negated with the `not.` prefix (for example `status=not.in.(draft,archived)`). Reserved characters (`,`, `(`, `)`) inside a value must be double-quoted PostgREST-style (for example `name=eq."a,b"`). See the [Postgres Changes subscription errors](#postgres-changes-subscription-errors) for the full list of supported operators, and the usage docs for [Postgres Changes](/docs/guides/realtime/postgres-changes?queryGroups=language&language=js#filtering-for-specific-changes). - `select`: Optional array of column names to restrict the change payload to a subset of columns instead of receiving the full row. Reduces payload size and the data transferred per event. The listed columns must be selectable by the subscribing role. Not supported for wildcard (`*`) schema or table subscriptions — an explicit `schema` and `table` are required. - `access_token`: Optional access token for authentication, if not provided, the server will use the API key. Example on protocol version `2.0.0`: ```json [ "3", "5", "realtime:chat-room", "phx_join", { "config": { "broadcast": { "ack": false, "self": true, "replay": { "since": 1763407103911, "limit": 10 } }, "presence": { "key": "user_id-827", "enabled": true }, "postgres_changes": [], "private": true } } ] ``` #### phx_leave This message is sent by the client to leave a channel. It can be used to clean up resources or stop listening for events on that channel. Payload should be empty object. Example on protocol version `2.0.0`: ```json ["1", "3", "realtime:avatar-stack-demo", "phx_leave", {}] ``` #### heartbeat The heartbeat message should be sent at least every 25 seconds to avoid a connection timeout. Payload should be an empty object. For heartbeat, the topic `phoenix` is used as this special message is not connected to a specific channel. Example on protocol version `2.0.0`: ```json [null, "26", "phoenix", "heartbeat", {}] ``` #### access_token Used to setup a new token to be used by Realtime for authentication and to refresh the token to prevent a private channel from closing when the token expires. ```ts { "access_token": string } ``` - `access_token`: The new access token to be used for authentication. Either to change it or to refresh it. Example on protocol version `2.0.0`: ```json [ "10", "1", "realtime:chat-room", "access_token", { "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30" } ] ``` #### broadcast (text frame) Used to send a broadcast event to all clients in a channel. The `payload` field contains the event name and the data to broadcast. ```ts { "event": string, "payload": json, "type": "broadcast" } ``` - `event`: The name of the user event to broadcast. - `payload`: The user data associated with the event, which can be any JSON-serializable data structure. - `type`: The type of message, which must always be `broadcast`. Example on protocol version `2.0.0`: ```json [ "10", "1", "realtime:chat-room", "broadcast", { "event": "user-event", "type": "broadcast", "payload": { "content": "Hello, World!", "createdAt": "2025-11-17T21:14:14Z", "id": "9b823349-71c0-465b-9a83-a63aa2a9ae6d", "username": "VCSHLD556nQD-B-vUTJJ3" } } ] ``` #### broadcast (binary frame) See the [User Broadcast Push](#user-broadcast-push) section for the binary frame structure. This message is a streamlined version of the text frame broadcast event that also supports non-JSON payloads. Below is the same example from the previous section, showing the binary frame structure with hexadecimal values for the header and plain text for the remaining fields: - Join Ref: `10` - Ref: `1` - Topic: `realtime:chat-room` - Payload encoding being JSON - User Event: `user-event` - Metadata is empty - User Payload ``` 0x03 // Type 0x02 // Join Ref Size 0x01 // Ref Size 0x12 // Topic Size 0x0A // User Event Size 0x00 // Metadata Size 0x01 // Payload Encoding (1 = JSON) 10 // Actual Join Ref 1 // Actual Ref realtime:chat-room // Topic user-event // User Event { // User Event Payload "content": "Hello, World!", "createdAt": "2025-11-17T21:14:14Z", "id": "9b823349-71c0-465b-9a83-a63aa2a9ae6d", "username": "VCSHLD556nQD-B-vUTJJ3" } ``` The payload encoding is a hint for the client to know if the payload should be treated as JSON or not. #### presence Used to send presence metadata after joining a channel. The payload contains the presence information to be tracked by the server. This metadata is then sent back to all clients in the channel via `presence_state` and `presence_diff` events. ```ts { "type": "presence", "event": "track", "payload": json } ``` Example on protocol version `2.0.0`: ```json [ "1", "5", "realtime:presence-room", "presence", { "type": "presence", "event": "track", "payload": { "name": "Alice", "color": "hsl(29, 100%, 70%)" } } ] ``` ### Server sent events | Event Type | Description | Requires Ref | Requires Join Ref | | ------------------ | ----------------------------------------------------------------------- | ------------ | ----------------- | | `phx_close` | Message from server to signal channel closed | ✅ | ✅ | | `phx_error` | Error message sent by the server when an error occurs | ✅ | ✅ | | `phx_reply` | Response to a `phx_join` or other requests | ✅ | ✅\* | | `system` | System messages to inform about the status of the Postgres subscription | ⛔ | ⛔ | | `broadcast` | Broadcast message sent to all clients in a channel | ⛔ | ⛔ | | `presence_state` | Presence state sent by the server on join | ⛔ | ⛔ | | `presence_diff` | Presence state diff update sent after a change in presence state | ⛔ | ⛔ | | `postgres_changes` | Postgres CDC message containing changes to the database | ⛔ | ⛔ | #### phx_close This message is sent by the server to signal that the channel has been closed. Payload will be empty object. Example on protocol version `2.0.0`: ```json ["3", "3", "realtime:avatar-stack-demo", "phx_close", {}] ``` #### phx_error This message is sent by the server when the channel process terminates unexpectedly. Payload will be an empty object. See [Reconnection](#reconnection) for recovery guidance. ```json ["3", "3", "realtime:avatar-stack-demo", "phx_error", {}] ``` #### phx_reply The server sends these messages in response to client requests that require acknowledgment. ```ts { "status": string, "response": any, } ``` - `status`: The status of the response, can be `ok` or `error`. - `response`: The response data, which can vary based on the event that was replied to `phx_join` has a specific response structure outlined below. When a join is rejected, `status` is `"error"` — see [Join errors](#join-errors) for the full list of error codes and recovery actions. Contains the status of the join request and any additional information requested in the `phx_join` payload. ```ts { "postgres_changes": [ { "id": number, "event": string, "schema": string, "table": string } ] } ``` - `postgres_changes`: Array of Postgres changes that the client is subscribed to, each object contains: - `id`: Unique identifier for the Postgres changes subscription - `event`: The type of event the client is subscribed to, such as `INSERT`, `UPDATE`, `DELETE`, or `*` - `schema`: The schema of the table the client is subscribed to - `table`: The table the client is subscribed to Example on protocol version `2.0.0`: ```json [ "1", "1", "realtime:chat-room", "phx_reply", { "status": "ok", "response": { "postgres_changes": [ { "id": 106243155, "event": "*", "schema": "public", "table": "test" } ] } } ] ``` #### system The server sends system messages to inform clients about the status of their Realtime channel subscriptions. See [Channel-level system errors](#channel-level-system-errors) for the full list of messages and recovery actions. ```ts { "message": string, "status": string, "extension": string, "channel": string } ``` - `message`: A human-readable message describing the status of the subscription. - `status`: The status of the subscription, can be `ok`, `error`, or `timeout`. - `extension`: The extension that sent the message. `postgres_changes` for Postgres Changes subscription status, or `system` for connection-level messages such as the replication-ready notification. - `channel`: The channel to which the message belongs, such as `realtime:room1`. Example on protocol version `2.0.0`: ```json [ "13", null, "realtime:chat-room", "system", { "message": "Subscribed to PostgreSQL", "status": "ok", "extension": "postgres_changes", "channel": "main" } ] ``` When a channel is joined with `config.broadcast.replication_ready` set to `true`, the server sends a `system` message with `extension: "system"` once the Postgres replication connection backing the channel is ready to stream changes. `status` is `"ok"` with `message: "Replication connection established"` on success, or `"error"` if the connection is not established in time (which also closes the channel — see [Channel-level system errors](#channel-level-system-errors)). ```json [ "14", null, "realtime:chat-room", "system", { "message": "Replication connection established", "status": "ok", "extension": "system", "channel": "main" } ] ``` #### broadcast (text frame) This is the structure of broadcast events received by all clients subscribed to a channel. The `payload` field contains the event name and data that was broadcasted. ```ts { "event": string, "meta" : { "id" : uuid, "replayed" : boolean }, "payload": json, "type": "broadcast" } ``` - `event`: The name of the user event to broadcast. - `meta`: Metadata about the broadcast message. Not always present. - `id`: A unique identifier for the broadcast message in UUID format. - `replayed`: A boolean indicating whether the message is a replayed message. Not always present - `payload`: The user data associated with the event, which can be any JSON-serializable data structure. - `type`: The type of message, which must always be `broadcast` for broadcast messages. Example on protocol version `2.0.0`: ```json [ null, null, "realtime:chat-room", "broadcast", { "event": "message", "type": "broadcast", "meta": { "id": "006554ce-d22d-469c-877a-88bef47214a3" }, "payload": { "id": "513edcc1-4cbc-4274-aa26-c195f7e8c090", "content": "oi", "username": "hpK9jN2iY-I2HioHWr5ml", "createdAt": "2025-11-18T22:44:29Z" } } ] ``` #### broadcast (binary frame) See the [User Broadcast](#user-broadcast) section for the binary frame structure. This message is a streamlined version of the text frame broadcast event that also supports non-JSON payloads. Below is the same example from the previous section, showing the binary frame structure with hexadecimal values for the header and plain text for the remaining fields: - Topic: `realtime:chat-room` - Payload encoding being JSON - Metadata: `{"id":"006554ce-d22d-469c-877a-88bef47214a3"}` - User Event: `message` - User Payload ``` 0x04 // Type 0x12 // Topic Size 0x07 // User Event Size 0x2D // Metadata Size 0x01 // Payload Encoding (1 = JSON) realtime:chat-room // Topic message // User Event {"id":"006554ce-d22d-469c-877a-88bef47214a3"} // Metadata { // User Event Payload "id": "513edcc1-4cbc-4274-aa26-c195f7e8c090", "content": "oi", "username": "hpK9jN2iY-I2HioHWr5ml", "createdAt": "2025-11-18T22:44:29Z" } ``` The metadata field is JSON encoded. The payload encoding is a hint for the client to know if the payload should be treated as JSON or not. #### postgres_changes The server sends this message when a database change occurs in a subscribed schema and table. The payload contains the details of the change, including the schema, table, event type, and the new and old records. ```ts { "ids": [ number ], "data": { "schema": string, "table": string, "commit_timestamp": string, "type": "*" | "INSERT" | "UPDATE" | "DELETE", "columns": [ { "name": string, "type": string } ] "record": { [key: string]: boolean | number | string | null }, "old_record": { [key: string]: boolean | number | string | null }, "errors": string | null } } ``` - `ids`: An array of unique identifiers matching the subscription when joining the channel. - `data`: An object containing the details of the change: - `schema`: The schema of the table where the change occurred. - `table`: The table where the change occurred. - `commit_timestamp`: The timestamp when the change was committed to the database. - `type`: The type of event that occurred, such as `INSERT`, `UPDATE`, `DELETE`, or `*` for all events. - `columns`: An array of objects representing the columns of the table, each containing: - `name`: The name of the column. - `type`: The data type of the column. - `record`: An object representing the new values after the change, with keys as column names and values as their corresponding values. - `old_record`: An object representing the old values before the change, with keys as column names and values as their corresponding values. - `errors`: Any errors that occurred during the change, if applicable. When the subscription was joined with a `select` array (see [phx_join](#phx_join)), `columns`, `record`, and `old_record` are restricted to the selected columns instead of the full row. ```json [ null, null, "realtime:chat-room", "postgres_changes", { "ids": [104868189], "data": { "schema": "public", "table": "test", "commit_timestamp": "2025-11-19T00:22:40.877Z", "type": "UPDATE", "columns": [ { "name": "id", "type": "int8" }, { "name": "created_at", "type": "timestamptz" }, { "name": "text", "type": "text" } ], "record": { "id": 46, "text": "content", "created_at": "2025-11-03T09:32:55+00:00" }, "old_record": { "id": 46 }, "errors": null } } ] ``` #### presence_state After joining, the server sends a `presence_state` message to a client with presence information. The payload field contains keys, where each key represents a client and its value is a JSON object containing information about that client. The key is defined by the client when joining the channel. If not specified, a UUID is automatically generated. ```ts { [key: string]: { metas: [ { phx_ref: string, [key: string]: any } ] } } ``` - `key`: The client key. - `metas`: An array of metadata objects for the client, each containing: - `phx_ref`: A unique reference ID for the metadata. - Any other custom fields defined by the client, such as `name`. Example on protocol version `2.0.0`: ```json [ "4", null, "realtime:cursor-room", "presence_state", { "2wCojG1xWgxG2ZxwocvSX": { "metas": [ { "phx_ref": "GHlA1fShRjMmZhnL", "color": "hsl(204, 100%, 70%)", "key": "2wCojG1xWgxG2ZxwocvSX" } ] }, "6eorYR7andHiq-7tCkmxQ": { "metas": [ { "phx_ref": "GHk99Q_ez6-GzaeG", "color": "hsl(7, 100%, 70%)", "key": "6eorYR7andHiq-7tCkmxQ" } ] }, "FOeQUamq3OLOWAAZK8iH3": { "metas": [ { "phx_ref": "GHk-wA8Z61GGzeoG", "color": "hsl(212, 100%, 70%)", "key": "FOeQUamq3OLOWAAZK8iH3" } ] } } ] ``` #### presence_diff After a change to the presence state, such as a client joining or leaving, the server sends a presence_diff message to update the client's view of the presence state. The payload field contains two keys, `joins` and `leaves`, which represent clients that have joined and left, respectively. Each key is either specified by the client when joining the channel or automatically generated as a UUID. ```ts { "joins": { [key: string]: { metas: [ { phx_ref: string, [key: string]: any } ] } }, "leaves": { [key: string]: { metas: [ { phx_ref: string, [key: string]: any } ] } } } ``` - `joins`: An object containing metadata for clients that have joined the channel, with keys as UUIDs and values as metadata objects. - `leaves`: An object containing metadata for clients that have left the channel, with keys as UUIDs and values as metadata objects. Example on protocol version `2.0.0`: ```json [ null, null, "realtime:cursor-room", "presence_diff", { "joins": { "XnAJXkZVEJuBYZcp9GCG5": { "metas": [ { "phx_ref": "GHlE8VLvxuKGzQJN", "color": "hsl(60, 100%, 70%)", "user": "123" } ] } }, "leaves": { "ouCsaiOdKZ9yauoy4x5pv": { "metas": [ { "phx_ref": "GHlE8HyhSPAmZgdB", "color": "hsl(72, 100%, 70%)", "user": "456" } ] } } } ] ``` ## Error handling Errors arrive on four channels: - A WebSocket close frame before the channel joins. - A `phx_reply` with `status: "error"` rejecting a `phx_join` or push. - A `system` event on a live channel — channel-level system errors are always followed by `phx_close`, while `postgres_changes` system errors are informational and leave the channel open. - A `phx_error` when the channel process terminates unexpectedly. ### Join errors When a `phx_join` is rejected, the `phx_reply` payload carries `response.reason` as `": "`. The server adds a backoff delay before replying, so avoid aggressive client-side retry loops on join errors. ```json { "status": "error", "response": { "reason": "InvalidJWTExpiration: Token has expired 300 seconds ago" } } ``` One exception: the `UnknownErrorOnChannel` code arrives as the bare human-readable string `"Unknown Error on Channel"` without the `: ` prefix. The JS client exposes the full `reason` string directly as the Error message without parsing it further. | Category | Error codes | Action | | -------------------- | ------------------------------------------------------------------------------------------------------------------ | ------------------------------- | | Auth — expired token | `InvalidJWTExpiration` (message contains `"expired"`) | Refresh token, rejoin | | Auth — invalid token | `MalformedJWT`, `JwtSignatureError`, `Unauthorized` | Do not retry; surface to caller | | Rate limit | `ConnectionRateLimitReached`, `ClientJoinRateLimitReached`, `ChannelRateLimitReached` | Backoff, reduce join frequency | | Database | `InitializingProjectConnection`, `IncreaseConnectionPool`, `DatabaseLackOfConnections`, `UnableToConnectToProject` | Retry with exponential backoff | | Config | `TopicNameRequired`, `TenantNotFound`, `RealtimeDisabledForTenant`, `RealtimeDisabledForConfiguration` | Do not retry | | Transient | `RealtimeRestarting` | Retry with backoff | ### Channel-level system errors `extension: "system"`, `status: "error"`. Match on the `message` field content — there is no machine-readable code field. Every channel-level system error is immediately followed by `phx_close`; the channel is closed. Client libraries should expose a way for users to subscribe to `system` events since there is no automatic handling. | Message contains | Cause | Recovery | | ---------------------------------------------------- | -------------------------------------------------------------------------------------------------- | ----------------------------- | | `Too many messages per second` | Broadcast/event rate limit | Throttle sends before rejoin | | `Too many presence messages per second` | Tenant presence rate limit | Reduce presence frequency | | `Client presence rate limit exceeded` | Per-client presence window | Longer cooldown before rejoin | | `Track message size exceeded` | Presence payload too large | Shrink payload | | `Token has expired` | JWT expired mid-session | Refresh token, rejoin | | `Fields \`role\` and \`exp\` are required in JWT` | Claims missing | Fix token issuance | | `Server requested disconnect` | Operational disconnect | Reconnect after delay | | `Replication connection was not established in time` | Replication connection not ready before the deadline (only when `replication_ready` was requested) | Retry with backoff | ### Postgres Changes subscription errors `extension: "postgres_changes"`. These do **not** close the channel — broadcast and presence continue. `status: "ok"` with `message: "Subscribed to PostgreSQL"` confirms the subscription is live. | Scenario | Server retries? | Client action | | ------------------------------------------------------ | ----------------- | -------------------------------------------------------------------------- | | Invalid filter operator | No | Fix params and rejoin | | Missing `schema`/`table` params | No | Fix params and rejoin | | Subscription insert failed (table/publication missing) | Yes, every 5–10 s | Surface as degraded state; wait or check Realtime is enabled for the table | | Database error during subscription | Yes, every 5–10 s | Surface as degraded state | | `"Too many database timeouts"` | No | Reduce subscription load; retry later | Supported filter operators: `eq`, `neq`, `lt`, `lte`, `gt`, `gte`, `in`, `like`, `ilike`, `is`, `match`, `imatch`, `isdistinct`. Any operator can be negated with the `not.` prefix (for example `id=not.eq.5`). Multiple conditions are combined with commas and applied as an `AND` (for example `col1=eq.val,col2=gt.5`). The `ids` array on incoming `postgres_changes` payloads must match the subscription IDs returned in the `phx_join` reply. A mismatch means inconsistent server/client state — tear down and rejoin. ### Broadcast errors Broadcast errors only affect **private channels**. When `config.broadcast.ack` is `false` (the default), all push failures — including size violations and RLS write denials — are silently dropped. RLS denials are always silent regardless of `ack`. When `ack` is `true`, the server replies on error with `response.error` (an atom string), not `response.reason`: ```json { "status": "error", "response": { "error": "payload_size_exceeded" } } ``` Note that the JS client (`send()`) resolves to the string `'error'` and does not expose the specific `error` atom to callers. ### Presence errors Push replies surface payload-shape errors with `reason: "Presence track payload must be a map"`. Other push-level failures (RLS write denied, unknown event type, internal errors) return `status: "error"` with no reason field. Presence rate-limit and size violations arrive as channel-level system errors (see above) and close the channel. ### Access token refresh Refresh the JWT in-band on private channels without rejoining using the `access_token` event: ```json ["10", "1", "realtime:my-channel", "access_token", { "access_token": "" }] ``` There is no reply on success. On failure, the server emits a `system` error and closes the channel. Tokens with the `sb_*` prefix are silently ignored by the server. ### Reconnection `phx_error` (unexpected server-side channel process termination, empty payload) should trigger a rejoin with exponential backoff. The JS client uses `[1000, 2000, 5000, 10000]` ms (capped at 10 s) configurable via `reconnectAfterMs`. `phx_close` following a rate-limit system error requires throttling before rejoin. Following a token system error, refresh the token first. A `phx_close` with no preceding system error is a clean close — only rejoin if it was unexpected. See [Limits](/docs/guides/realtime/limits) for the per-tenant thresholds that trigger rate-limit errors.