# WebSocket Events

> Push live updates to web and mobile apps with API Maker - connect a WebSocket with the tokens, subscribe to the answers of tables, custom and system APIs or to your own events with a condition, and emit from your code with emitEventWS.

Source: https://docs.apimaker.dev/v4/docs/pages/web-socket-event-page.html

API Maker runs a WebSocket server on the port of its APIs (`38246` on a server set up by the install script, `wss://` behind Caddy). A client connects with its tokens, subscribes to what it wants to hear about, and receives a message every time a matching API succeeds on any server of the cluster, or when your code emits a custom event. No polling, no message broker of your own.

> Diagram : WebSockets : connect with the tokens, register with a condition, get notified from any server
>
> WebSocket notifications in API Maker : a browser or mobile app opens a WebSocket on the port of the APIs (38246) with its tokens in the query string, receives a CONNECTED frame, sends a REGISTER frame naming the APIs, tables or custom events it wants with a condition, and gets NOTIFICATION frames when a matching API succeeds on any server of the cluster or when code emits the event ; Redis carries the notifications between servers.

## 1. Connect

The tokens go in the query string of the WebSocket URL, with the same names as the [request headers](https://docs.apimaker.dev/v4/docs/apis-all/header/requestHeader.html).

```javascript
const ws = new WebSocket(
    'wss://ws.example.com/'                                  // the WebSocket URL of your API Maker
    + '?x-am-authorization=' + encodeURIComponent(apiUserToken)
    + '&x-am-user-authorization=' + encodeURIComponent(personToken)   // when your events need the person
);
```

- `x-am-authorization` : the API user, always. The person token of the [auth provider](https://docs.apimaker.dev/v4/docs/authorization/AMDB.html) of the events you subscribe to : `x-am-user-authorization`, `x-google-authorization`, `x-azure-authorization`, `x-aws-authorization` or `x-custom-authorization`. API Maker finds out which provider a token belongs to ; `&authProviders=name1,name2` narrows it when you want.
- The first message from the server is `CONNECTED` :

```json
{ "type": "CONNECTED", "response": { "connected": true } }
```

- A bad token closes the connection with a `TOKEN_VALIDATION` message which says which header is wrong, then the close code `1000` with the reason `Invalid client`.

## 2. Subscribe

Send a `REGISTER` message with the events you want, then read the answer : the ones accepted, with their `eventId`, and the ones refused with the reason.

```javascript
ws.onmessage = (e) => {
    const msg = JSON.parse(e.data);
    if (msg.type === 'CONNECTED') {
        ws.send(JSON.stringify({
            objType: 'REGISTER',
            onEvents: [
                {   // every successful get all on this table
                    eventType: 'INSTANCES', apiName: 'SCHEMA_GET_ALL',
                    instance: 'mongodb', database: 'shop', collection: 'orders',
                    getEventData: true, select: { order_no: 1, status: 1 },
                },
                {   // a custom event, only when the criteria matches the emitted data
                    eventType: 'CUSTOM_WS_EVENTS', apiName: 'default/orders-updated',
                    condition: { conditionType: 'RESPONSE', criteria: { recipientId: myUserId } },
                    getEventData: true,
                },
            ],
        }));
    } else if (msg.type === 'REGISTER') {
        console.log(msg.response.validOnEvents, msg.response.invalidOnEvents);   // eventId per subscription
    } else if (msg.type === 'NOTIFICATION') {
        render(msg.response.eventId, msg.response.eventData);
    }
};
```

| Key of an event | Meaning |
|---|---|
| `eventType` | `INSTANCES`, `CUSTOM_APIS`, `SYSTEM_APIS` or `CUSTOM_WS_EVENTS`. |
| `apiName` | The [API id](https://docs.apimaker.dev/v4/docs/apis-all/overview.html#api-ids) for a table (`SCHEMA_GET_ALL`, `GEN_POST_BULK_INSERT`…), the name of the custom API, the id of the system API (`ENCRYPT_DATA`…), or the name of the custom WebSocket event. |
| `instance`, `database`, `collection` or `table` | The table, for `INSTANCES`. |
| `condition` | `{ conditionType: 'RESPONSE', criteria: { field: value } }` : only the answers whose data has these values reach this client. For a custom event, the emitted data must match the criteria exactly, which is how one notification reaches one person. |
| `select` | The fields of the data to send, `{ field: 1 }`. |
| `getEventData` | `true` sends the data ; `false` sends only the fact that it happened. |

- Subscriptions are checked against the groups of the API user and the auth provider of the event : `invalidOnEvents` says why one was refused.

## 3. Receive

```json
{ "type": "NOTIFICATION", "response": { "eventId": "…", "eventData": [ { "order_no": 1001, "status": "PAID" } ] } }
```

- `eventId` is the one of the register answer : it says which subscription the notification is for. `eventData` is there when the subscription asked for it with `getEventData`.
- Notifications come from **any server** of the cluster : Redis passes them between the servers.
- Unsubscribe with the `eventId` of the register answer :

```javascript
ws.send(JSON.stringify({ objType: 'UNREGISTER', onEvents: [ eventId ] }));
```

## When a notification is sent

- After every **successful** call of a subscribed API : the answer of the call is the data, filtered by `condition` and `select`. The call can come over HTTP or from your code (custom APIs, hooks, events…).
- Not when an error happens before the answer exists : a pre hook which throws sends nothing. A post hook which throws does not stop the notification.
- For a custom WebSocket event, when your code emits it :

**TypeScript**

```typescript
await g.sys.system.emitEventWS('default/orders-updated', { recipientId: '42', type: 'order-updated', order_no: 1001 });
```

**Python**

```python
await g.sys.system.emit_event_ws('default/orders-updated', {'recipientId': '42', 'type': 'order-updated', 'order_no': 1001})
```

**PHP**

```php
$g->sys->system->emitEventWS('default/orders-updated', ['recipientId' => '42', 'type' => 'order-updated', 'order_no' => 1001]);
```

**C#**

```csharp
await g.Sys.System.EmitEventWS("default/orders-updated", new JsonObject { ["recipientId"] = "42", ["type"] = "order-updated", ["order_no"] = 1001 });
```

**Go**

```go
if _, err := g.Sys.System.EmitEventWS("default/orders-updated", am.M{"recipientId": "42", "type": "order-updated", "order_no": 1001}); err != nil {
	return nil, err
}
```

**Rust**

```rust
g.sys.system.emit_event_ws("default/orders-updated", json!({ "recipientId": "42", "type": "order-updated", "order_no": 1001 }))?;
```

**Java**

```java
g.sys.system.emitEventWS("default/orders-updated", Map.of("recipientId", "42", "type", "order-updated", "order_no", 1001));
```

**Kotlin**

```kotlin
g.sys.system.emitEventWS("default/orders-updated", mapOf("recipientId" to "42", "type" to "order-updated", "order_no" to 1001))
```

```text
POST /api/system-api/admin/emit-event-ws
{ "name": "default/orders-updated", "eventData": { "recipientId": "42", "order_no": 1001 } }
```

- To notify N people, emit once per person with their id in the data ; each client subscribes with its id in `criteria`.

## Custom WebSocket events

`APIs & Logic → WebSockets` lists the event names of the account. A custom event needs to exist there before it can be subscribed to or emitted.

| Field | Meaning |
|---|---|
| Name | The `apiName` of the subscription and the name given to `emitEventWS`. |
| Auth providers | The provider whose token a client must have validated to subscribe. Empty : the first auth provider of the account. |
| Can user connect code | A function which decides whether a client may subscribe : return `{ canConnect: true }` or `{ canConnect: false, errorText: '…' }`. It sees the tokens in `g.req.auth`. |
| Trigger on API | Optional : an API whose success also emits this event. |

## Who may subscribe

A subscription is taken when all of these hold, and `invalidOnEvents` says which one did not :

- **The groups of the API user grant the event** : every WebSocket event, or the ones the [group](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.html) names. That goes for the events of tables, of custom and system APIs, and for the custom WebSocket events of the account alike.
- **The client connected with a token of an auth provider of the event**, when the account has auth providers.
- **The can user connect code of the event says yes**, when the event has one.

What a client proved when it connected is looked at again while it stays connected :

| What changes | What the client sees |
|---|---|
| The API user is deactivated or removed, its password changes, or the account is deactivated. | A `TOKEN_VALIDATION` message which says why, then the close code `1000` (`Invalid client`). Its subscriptions are gone. |
| A token the client connected with runs out : the token of the API user, or its person token. | A `TOKEN_VALIDATION` message with the code `401` which names the token, then the close code `1000` (`Invalid client`). Its subscriptions are gone. See [Staying connected](https://docs.apimaker.dev/v4/docs/pages/web-socket-event-page.html#staying-connected). |
| The root user changes the secret the tokens of API users are signed with (root user settings). | A `TOKEN_VALIDATION` message with the code `401` which names the token, then the close code `1000` (`Invalid client`), for every client which connected with a token of the secret before. Its subscriptions are gone. See [Staying connected](https://docs.apimaker.dev/v4/docs/pages/web-socket-event-page.html#staying-connected). |
| A group stops granting an event, or the API user leaves the group. | `{ "type": "UNREGISTER", "response": { "eventId": "…" } }` for each subscription which is taken back. The connection and the other subscriptions stay. |
| A group starts granting an event. | Nothing : the client can subscribe to it from then on, without connecting again. |
| A custom WebSocket event is removed. | `{ "type": "UNREGISTER", "response": { "eventId": "…" } }` for its subscriptions. |

- A connection lasts as long as the first of its tokens : until the `exp` of the token of the API user and of the person token it connected with. A Google token counts five minutes longer, as its check does ; a token checked by the code of a custom provider counts until the `exp` that code answers, when it answers one. A token without an end closes nothing.

## The connection

- **Ping** : the server pings every client once a minute. A WebSocket client answers by itself (browsers and the usual libraries do, there is nothing to write), which keeps proxies from closing a quiet connection. A client may stay silent for hours ; one which answers nothing at all for two and a half minutes is taken for gone and closed.
- **Close codes** : `1000` `Invalid client` when the tokens do not let the client in, or do not any more (one of them ran out, the API user changed) ; `1001` `Server is going away` when the server stops or restarts, which is no refusal.
- **Reconnect** : subscriptions belong to the connection. When it closes, connect again and send `REGISTER` again ; after `1001` another server of the cluster takes the client at once. See [Staying connected](https://docs.apimaker.dev/v4/docs/pages/web-socket-event-page.html#staying-connected).
- **Size** : a message of a client is at most 16 MiB (a `REGISTER` is a few hundred bytes). A longer one ends the connection.
- **A client which does not read** : what it is told waits for it up to 64 MiB, and is skipped past that until it reads again. The connection stays.
- **Many clients** : a client which waits costs a little memory and almost no CPU. How many a server holds, and what to look at before it holds them, is in [Many clients](https://docs.apimaker.dev/v4/docs/pages/web-socket-event-page.html#many-clients).

## Staying connected

A client stays connected for as long as it is reachable and its tokens are valid. It sends nothing for that : no request to repeat, no heartbeat of its own. The answers to the pings of the server are sent by the browser or the WebSocket library.

| What closes a connection | What the client sees | What it does |
|---|---|---|
| One of its tokens runs out. | A `TOKEN_VALIDATION` message with the code `401` which names the token, then the close code `1000`. | Gets a new token (with the [refresh token](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-token-api.html), or a new login), connects again and sends `REGISTER` again. |
| The API user or the account is deactivated, or the password of the API user changes. | A `TOKEN_VALIDATION` message which says why, then the close code `1000`. | The same, once it has a token which is valid again. |
| The root user changes the secret the tokens of API users are signed with. | A `TOKEN_VALIDATION` message with the code `401` which names the token, then the close code `1000`. | Gets a new token, which is signed with the new secret, connects again and sends `REGISTER` again. A token of the secret before, and its refresh token, are not accepted any more : a new login. |
| The server stops or restarts. | The close code `1001` (`Server is going away`). | Connects again with the same tokens : another server of the cluster takes it at once. |
| Its machine or its network is gone : nothing came from it for two and a half minutes, not even the answers to the pings. | Nothing : it is not there. | Connects again when it is back. |

**A token ran out**

```json
{ "type": "TOKEN_VALIDATION", "errors": [ { "code": 401, "message": "The token of 'x-am-authorization' expired : connect again with a new token." } ] }
```

**The secret of the tokens changed**

```json
{ "type": "TOKEN_VALIDATION", "errors": [ { "code": 401, "message": "The secret the token of 'x-am-authorization' is signed with changed : connect again with a new token." } ] }
```

One handler covers all five : when the socket closes, come back with a token which is valid at that moment, and subscribe again.

```javascript
function connect() {
    getToken().then((token) => {                        // yours : a token which is valid now, renewed when it has to be
        const ws = new WebSocket('wss://ws.example.com/?x-am-authorization=' + encodeURIComponent(token));
        ws.onmessage = (e) => {
            const msg = JSON.parse(e.data);
            if (msg.type === 'CONNECTED') ws.send(JSON.stringify({ objType: 'REGISTER', onEvents: myEvents }));
            else if (msg.type === 'NOTIFICATION') render(msg.response.eventId, msg.response.eventData);
        };
        // Whatever closed it : come back, and subscribe again. Not all at the same moment : a second, and up to four more.
        ws.onclose = () => setTimeout(connect, 1000 + Math.random() * 4000);
    });
}
connect();
```

- **Wait a random moment before connecting again** : when a server stops, every client it held sees the close at the same instant. Clients which all come back after exactly one second arrive together ; with a random wait they arrive spread over a few seconds, and each one is let in sooner. See [Many clients](https://docs.apimaker.dev/v4/docs/pages/web-socket-event-page.html#many-clients).

- **How long a token lasts** : `expires_in` of the [token answer](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-token-api.html), 72 hours unless `expiresInSeconds` or the configuration say otherwise. The tokens of Google, Azure AD and AWS Cognito usually last an hour.
- **Nothing is sent again** : what happens between the close and the new `REGISTER` does not reach the client afterwards. A client which may miss nothing opens a second connection with its new token a little before the old token runs out, sends `REGISTER` there, and closes the first one once the second answered.

## Many clients

A client which waits costs memory, and almost nothing else : its socket, its tokens and its subscriptions. How many clients a server holds is a question of its memory, not of its CPU.

| Server | Clients connected at once | Memory per client | New clients a second | A notification, p50 |
|---|--:|--:|--:|--:|
| [1 shared vCPU · 1 GB](https://docs.apimaker.dev/v4/docs/benchmarks/websockets/linode-shared-1-vcpu-1gb.html) | **26,484** | 8.6 KB | 2,602 | 2.51 ms |
| [2 shared vCPUs · 4 GB](https://docs.apimaker.dev/v4/docs/benchmarks/websockets/linode-shared-2-vcpu-4gb.html) | **338,016** | 8.5 KB | 4,041 | 1.45 ms |
| [4 shared vCPUs · 8 GB](https://docs.apimaker.dev/v4/docs/benchmarks/websockets/linode-shared-4-vcpu-8gb.html) | **801,972** | 8.7 KB | 7,009 | 1.06 ms |
| [8 shared vCPUs · 32 GB](https://docs.apimaker.dev/v4/docs/benchmarks/websockets/linode-shared-8-vcpu-32gb.html) | **3,662,564** | 8.8 KB | 10,939 | 0.94 ms |
| [2 dedicated vCPUs · 4 GB](https://docs.apimaker.dev/v4/docs/benchmarks/websockets/linode-dedicated-2-vcpu-4gb.html) | **338,006** | 8.5 KB | 3,464 | 1.74 ms |
| [4 dedicated vCPUs · 8 GB](https://docs.apimaker.dev/v4/docs/benchmarks/websockets/linode-dedicated-4-vcpu-8gb.html) | **795,888** | 8.7 KB | 6,627 | 1.04 ms |
| [8 dedicated vCPUs · 16 GB](https://docs.apimaker.dev/v4/docs/benchmarks/websockets/linode-dedicated-8-vcpu-16gb.html) | **1,732,035** | 8.8 KB | 10,093 | 0.83 ms |

Measured on 14 October 2026 with API Maker 4.0.0 on Linode servers set up by the install script, MongoDB and Redis on the same server. Every client connected with both tokens and one subscription ; « new clients a second » connect, subscribe and leave ; the notification is for one person, from the HTTP call which emits it to the client. As API servers which share one Redis : 2,022,026 clients on 1 × 8 vCPU · 16 GB, 1,915,960 clients on 2 × 4 vCPU · 8 GB, 1,619,372 clients on 4 × 2 vCPU · 4 GB. Every run, with its memory second by second : [WebSocket benchmarks](https://docs.apimaker.dev/v4/docs/benchmarks/websockets/index.html).

- **Count the servers from the memory** : the clients one server holds, and one server more as a spare. Every server of a cluster takes clients, and all of them share the Redis of API Maker, which carries each notification to the server which holds the client : a load balancer needs no sticky session. [Scale to N users](https://docs.apimaker.dev/v4/docs/benchmarks/websockets/scaling.html) has the picture and the rule.
- **Open files** : every connected client is one open file of the server. The install script lets the service open as many as the firewall of the machine tracks connections : 256 for each MB of memory, 1,048,576 at the least (`LimitNOFILE` : about 4 million on a server of 16 GB) ; started any other way, API Maker raises its own limit as far as the system allows, and says in its log how many connections it takes. The WebSockets never take all of them : a sixteenth of the connections (64 at the least, 4,096 at the most) is kept for the calls of your APIs, which would else find no connection while the clients stay. A client which comes when the server holds as many WebSockets as it takes is answered `503` with `Retry-After: 5` : it comes back, as after any close.
- **The connection table of the firewall** : a Linux server which runs Docker tracks every connection (`nf_conntrack`), and a connection which closed stays in the table for a while. With its default size (262,144 connections) the table can fill before the memory does when many clients come and go, and the kernel then drops new connections without a word to anybody (`nf_conntrack: table full, dropping packet` in `dmesg`). The install script sizes the table from the memory of the server (`/etc/sysctl.d/90-api-maker.conf`), and API Maker warns in its log at start when the table is smaller than the clients the server could hold.
- **Clients which all come back together** : after a restart, or when a server of a cluster stops, the clients it held connect again within the same seconds. API Maker lets 256 of them in at a time (the check of their tokens), handles the messages of 256 clients of an account at a time, and accepts no more than 1,024 connections ahead of the ones it is serving : the others wait for their turn as sockets, their request unread in the kernel, about 4 KB each. That is memory on top of what the clients take once they are in : a server with a quarter of its memory free has its clients back in seconds or minutes, one full to its last 5 % lets them back a few hundred at a time (8 GB and 800,000 clients) : the [benchmarks](https://docs.apimaker.dev/v4/docs/benchmarks/websockets/methodology.html#restart) restart a server with three quarters of the clients it holds. A client which waits a random moment before it connects again helps too (see [Staying connected](https://docs.apimaker.dev/v4/docs/pages/web-socket-event-page.html#staying-connected)).
- **Clients which all go together** : when a network or a load balancer goes, every client of a server leaves in the same second. Their sockets are closed at once, and Redis is told behind them.
- **A server which is killed** : a server which stops takes its clients out of Redis first. One which is killed, or whose machine goes, cannot. So every server says every ten seconds that it is alive, and once the word of one of them ran out (45 seconds), another server takes out of Redis what it left, or the same one does when it comes back. Within a minute the calls of your APIs do not find its clients any more, and do not count them among the clients they told. The clients themselves connect again, as after any close.
- **Calls which come faster than the clients are told** : an API answers before its clients are told, and a call takes far less than telling a thousand clients. Past 16,384 notifications which wait to be told, for each thread of the server, the next call whose answer is told to clients waits before it is answered, ten seconds at the most. Callers are slowed down to the pace their clients are told at : the memory of the server stays level, and a notification is not late by all the work which piled up before it.
- **The memory of the network** : Linux gives all its TCP connections together a part of the memory of the machine (`net.ipv4.tcp_mem`, about 9 % as it sets it up). A connection which holds a short message nobody read yet, or one its client did not acknowledge yet, holds a page of it : nothing for a few thousand clients, all of it when hundreds of thousands of them do something in the same second, and Linux then slows every connection of the machine down. API Maker looks at its open connections once a second and raises that memory as they come, two pages for each connection : only upwards, never above a quarter of the memory of the machine ([`tcpMemoryPercent`](https://docs.apimaker.dev/v4/docs/am-resources/api-maker-configurations.html), 0 to leave the kernel alone), and not at all on a server with fewer connections than the kernel has room for. It says so in its log ; when it may not write the numbers (it is not root, or runs in a container), it says the `sysctl` line to run by hand.
- **Leave room** : a server which is full of clients has no memory left for the work of its APIs. Keep a quarter of the memory free : the clients of a server which stops then fit on the others.

## From your code

- `g.sys.system.emitEventWS(name, data)` from any custom API, hook, event, scheduler or migration.
- The sample custom API `/default/ws-notify` of a new account has the complete client snippet in its comments.
- The [system API](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-emit-event-ws-api.html) does the same over HTTP.

## Good to know

- The admin panel and the local client use the same server with their own tokens : what they ask and what they are told (the files of the account) is for them only, an API user is refused it.
- Groups grant WebSocket events : an API user subscribes to a custom event only when a group allows it.
- The WebSocket is on the port of the APIs : at the address of `BE_HOST_PORT`, with `ws://` for `http://` and `wss://` for `https://`. Behind Caddy, it is on the domain of the APIs.
