# Connection

Realtime events tell a client that something changed: a new message, a read tick, a call, a presence change. They arrive on one WebSocket per session. A socket drops for many reasons (a phone sleeps, a network changes, the 24-hour cap), and events sent while it is down are not replayed. So a client needs two things: a socket that comes back on its own, and a [catch-up](/realtime/sync/) over HTTP after every gap.

## How it works

### Where to connect

[`POST /v1/session`](/api/operations/exchange-session/), [`POST /v1/session/refresh`](/api/operations/refresh-session/) and, for a headless client your server signs in, [`POST /v1/server/sessions`](/api/operations/mint-server-session/) return `realtime`:

| Field | Meaning |
|---|---|
| `http_host` | The host you put in every `authorization` object. |
| `ws_url` | The WebSocket URL to open. |
| `channel` | Your user's own channel, `/users/{user_id}`. |

The socket is AWS AppSync Events. Only the API publishes. A client can connect and subscribe, never publish.

### Handshake

1. Open `ws_url` with two subprotocols: `aws-appsync-event-ws` and `header-` followed by the base64url of `{host: http_host, Authorization: session_token}`.
2. When the socket opens, send `{type: "connection_init"}`.
3. On `connection_ack`, subscribe to your channel. The `id` is one you pick, unique per subscription on this socket.
4. On `subscribe_success` for that `id`, the socket is live.

```json
{"type": "subscribe", "id": "<your subscription id>", "channel": "/users/{user_id}",
 "authorization": {"host": "<http_host>", "Authorization": "<session_token>"}}
```

Events then arrive as messages of type `data`. Their `id` is the subscription id, and `event` holds one JSON string or an array of them. Each string parses to the envelope:

```json
{"type": "message.created", "id": "<UUIDv7>", "ts": "<time>", "data": {}}
```

The [event catalog](/reference/events/) lists every `type` and its `data`.

### Who may subscribe

The authorizer allows a connection for any valid session token. It allows a subscribe to `/users/{user_id}` only with that user's session token, and to `/presence/{user_id}` only with a presence pass for that user (see [presence subscriptions](/realtime/presence/)). It never allows a publish. It caches its answer for 300 s, capped at the token's remaining life.

### Keep-alive

AppSync sends a keep-alive message every 60 s. `connection_ack` carries `connectionTimeoutMs`. The frame closes a socket that hears nothing for that long (300000 ms when the field is missing) and reconnects.

A page that slept may keep a socket that looks open but is dead. When the page becomes visible again, the frame checks when it last heard anything. After 90 s of silence it opens a new socket and runs a catch-up. Otherwise it only runs the catch-up.

### Reconnect

When a socket closes, the frame waits a random time between 0 and `min(30 s, 1 s x 2^attempt)`, then connects again. `attempt` goes back to 0 once a subscribe succeeds. When the browser fires `online`, the frame reconnects at once.

When the first subscribe of a session succeeds, and after a reconnect that followed a gap, the frame runs a [catch-up](/realtime/sync/). Loads that started before the socket was live can miss events published meanwhile. A new socket opened after a token refresh needs none.

### Re-authenticate after a refresh

A session token lasts 900 s. The frame refreshes it every 8 minutes with [`POST /v1/session/refresh`](/api/operations/refresh-session/). After each refresh it opens a new socket with the new token. When that socket's subscribe succeeds, it becomes the live one, its presence channels are subscribed again, and the old socket closes. The old socket stays live until then, so no events are lost and no catch-up runs.

### What the frame does on each message

| Message | What the frame does |
|---|---|
| `connection_ack` | Reads `connectionTimeoutMs`, subscribes to `channel`. |
| `subscribe_success` for its channel | Marks the socket live, subscribes its presence channels, runs a catch-up if there was a gap. |
| `data` | Parses each event, drops an `id` it has seen, handles the rest. |
| `subscribe_error` for its channel | Closes the socket, which reconnects with backoff. |
| `subscribe_error` for a presence channel | Keeps the socket open; see [presence subscriptions](/realtime/presence/). |
| `connection_error` or `error` | Closes the socket, which reconnects with backoff. |

### Session end

`session.revoked` ends the session. Its `data` is `{reason, temporary}`: `reason` is `banned`, `deleted` or `merged`, and `temporary` is true for a ban that expires.

### Order and duplicates

Events are not ordered and the same event can arrive more than once. Deduplicate on the envelope `id` and keep the copy with the higher `seq`. [Ordering and limits](/realtime/ordering/) has the rules.

## Limits

- AppSync closes every socket after 24 hours. Treat it as any other drop.
- At most 200 subscriptions per socket.
- A session token lasts 900 s; reconnect with the fresh one after each refresh.
- The API sends at most 5 events per publish, each under 240 KB, so `event` can hold an array.

## Where it ends

- AppSync does not replay events. Anything sent while your socket was down is lost to the socket. Recover it over HTTP with a [catch-up](/realtime/sync/).
- A client cannot publish. Every change goes through the HTTP API, which publishes the event.
- Typing events are ephemeral. A missed one is gone and needs no recovery.

## Related topics

- [Event catalog](/reference/events/)
- [Sync and recovery](/realtime/sync/)
- [Presence subscriptions](/realtime/presence/)
- [Ordering and limits](/realtime/ordering/)
- [Sessions and tokens](/guides/sessions-and-tokens/)
- [`POST /v1/session`](/api/operations/exchange-session/)
- [`POST /v1/session/refresh`](/api/operations/refresh-session/)
- [`POST /v1/server/sessions`](/api/operations/mint-server-session/)