# Ordering and limits

Events are not ordered, and the same change can reach a client more than once: twice on the socket, or once on the socket and once in a catch-up. Three rules make every copy safe to apply: deduplicate on `id`, keep the higher `seq`, and move pointers forward only.

## How it works

### Deduplicate on `id`

Every event arrives in the envelope `{type, id, ts, data}`. `id` is a UUIDv7. Drop an event whose `id` you have already handled. With `presence.fanout` on, `presence.changed` can arrive twice, on the peer's presence channel and on your own channel, with the same `id`.

The frame remembers the last 1000 event ids.

### Keep the copy with the higher `seq`

`message.created`, `message.updated` and `conversation.updated` carry the `seq` of their `Message` or `ConversationSummary`, and catch-up rows carry it too. When two copies of the same message or inbox row meet, keep the one with the higher `seq`. Never replace a copy with one of a lower `seq`, whether it comes from an event or a catch-up.

- A message's `seq` is its place in its conversation's sync order.
- An inbox row's `seq` is the viewer's inbox seq for that row.

### Pointers only move forward

`read.updated` and `delivered.updated` carry a message id: `last_read_message_id` or `last_delivered_message_id`. Read it as "everything up to this message". Message ids are UUIDv7 and sort by creation as strings, so mark each of your own messages with an id less than or equal to the pointer, and never unmark one. A late event with an older pointer then changes nothing.

The `participants` pointers in a [thread catch-up](/realtime/sync/) work the same way.

### `seq` has gaps

`seq` counts every change, including ones you never see: a message the other side deleted for themselves, a send that was dropped silently. Never infer missing data from a gap. Ask the [catch-up routes](/realtime/sync/) for what changed instead.

### Events without `seq`

The other events carry no `seq`: `read.updated`, `delivered.updated`, `typing`, `presence.changed`, `photos.updated`, `call.ringing`, `call.updated`, `call.capture`, `purchase.updated` and `session.revoked`. Deduplicate them on `id`. For calls and purchases, the [recovery calls](/realtime/sync/) give the current state after a gap.

## Limits

| Limit | Value |
|---|---|
| Subscriptions per socket | 200 |
| Presence subscriptions the frame keeps | 100 |
| Socket lifetime | 24 hours |
| Keep-alive from AppSync | every 60 s |
| Session token lifetime | 900 s |
| Presence pass lifetime | 1 hour |
| Events per publish | 5, each under 240 KB |
| `GET /v1/conversations?changed_since` page | `limit` at most 50 |
| `GET /v1/conversations/{id}/changes` page | `limit` 1 to 200, default 100 |
| Typing | send at most every 3 s, show for 5 s |

## Where it ends

- `seq` only compares copies of the same message or the same inbox row. Events of different types have no order between them.
- `seq` does not tell you what is missing, only which copy is newer.
- Presence, typing and settings changes take no `seq`. Recover presence by reloading it, as [presence subscriptions](/realtime/presence/) explains.

## Related topics

- [Event catalog](/reference/events/)
- [`GET /v1/conversations/{id}/changes`](/api/operations/message-changes/)
- [`POST /v1/conversations/{id}/read`](/api/operations/mark-read/)
- [`POST /v1/conversations/{id}/delivered`](/api/operations/mark-delivered/)
- [Connection](/realtime/connection/)
- [Sync and recovery](/realtime/sync/)
- [Presence subscriptions](/realtime/presence/)