---
title: "Realtime stream"
description: "One Pusher-protocol connection per backend instance"
url: https://docs.gleap.ai/en/articles/723-realtime-stream
collection: "Guide"
updated: 2026-11-01
language: en
---

# Realtime stream

One Pusher-protocol connection per backend instance

The stream is the **recommended event channel**: your backend holds **one outbound WebSocket** to Gleap's realtime cluster and receives every event for your project on it — no inbound endpoint to expose, no per-delivery signature verification, no retry windows, and lower latency than [webhooks](https://gleap-developer-docs.gleap.help/en/articles/722-webhooks-and-events) (which queue and retry per delivery). The stream speaks the Pusher wire protocol, so any mature off-the-shelf Pusher client (Node, Java, Go, .NET) works unmodified. Webhooks remain fully supported when your infrastructure policy prefers inbound HTTP.

## Connect

The API host depends on the [data region](https://gleap-developer-docs.gleap.help/en/articles/711-data-regions) of your project:

<table style="min-width: 50px;"><colgroup><col style="min-width: 25px;"><col style="min-width: 25px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Data region</p></th><th colspan="1" rowspan="1"><p>API host</p></th></tr><tr><td colspan="1" rowspan="1"><p>EU (default)</p></td><td colspan="1" rowspan="1"><p><code>https://api.gleap.io</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>US</p></td><td colspan="1" rowspan="1"><p><code>https://api.us.gleap.ai</code></p></td></tr></tbody></table>

> **Info:** The examples on this page use the EU host. If your project lives in another region, replace `https://api.gleap.io` with your region's host. API tokens are per project and only valid in the project's region.

Read the connection parameters at startup — never hardcode them:

```bash
curl https://api.gleap.io/v3/s2s/stream/config -H "Authorization: Bearer $TOKEN"
```

```json
{ "protocol": "pusher", "host": "sockets.gleap.io", "port": 443, "tls": true,
  "appKey": "…", "channel": "private-s2s-<projectId>",
  "authEndpoint": "https://api.gleap.io/v3/s2s/stream/auth",
  "userAuthEndpoint": "https://api.gleap.io/v3/s2s/stream/user-auth" }
```

Point your client's **channel authorization** at `authEndpoint` (with your token as the `Authorization` header) and subscribe to `channel`. Include two custom auth params:

```json
{ "consumerId": "backend-pod-3", "typingEvents": false }
```

-   `consumerId` names the connecting instance (`[A-Za-z0-9_-]{1,64}`).
    
-   `typingEvents: true` opts this stream into `agent.typing.*` (high volume; default off).
    

Also configure the client's **user authentication** against `userAuthEndpoint` (Pusher "signin") — that is what allows the server to cleanly terminate a superseded connection during takeover.

Event `data` arrives as a JSON string of the same envelope webhooks receive — `JSON.parse` it (Pusher clients usually do this for you). Payloads larger than the socket limit arrive as a slim pointer (`data.truncated: true` with the conversation/message id) — fetch the full object over REST.

## Exactly one consumer

Gleap allows **one active** `consumerId` **per project**. When a connection authorizes with a _different_ `consumerId`, it takes over: the previous consumer's connections are closed and a `s2s.takeover` control event is published on the channel.

Connections that share the **same** `consumerId` are the same consumer: none of them is taken over, and **each receives every event**. Use this to run the stream on several instances at once (active-active), so losing one instance loses no events. Deduplicate by the envelope `id` (for example `SET NX` in Redis with a short TTL) or make processing idempotent. `typingEvents` is a project-wide setting and the most recent auth wins, so use the same value on every instance.

Rolling deployments need no coordination either way: with the same `consumerId` old and new pods overlap and both receive events; with a new `consumerId` the new pod takes over and the old one is dropped.

### Detecting a takeover

`s2s.takeover` goes out at the same moment as the disconnect, so it is best effort: it may arrive just before the close or not at all. It is a control event, not wrapped in the envelope:

```json
{ "event": "s2s.takeover", "channel": "private-s2s-<projectId>",
  "data": "{\"consumerId\":\"<consumerId of the new consumer>\"}" }
```

Every subscriber sees it, so compare `consumerId` with your own and ignore it when it matches. A taken-over connection is closed with code `4009` and reason `You got disconnected by the app.`, which only reaches connections that completed user authentication.

You were taken over when you receive `s2s.takeover` with a foreign `consumerId`, or a close `4009` with that reason. Do not reconnect automatically in that case: two instances with different ids would keep evicting each other. Reconnect on every other close.

## Disconnects

Every close initiated by the realtime server first sends a `pusher:error` message (`{ "code", "message" }`) and then a close frame with the same code. Log both.

<table style="min-width: 100px;"><colgroup><col style="min-width: 25px;"><col style="min-width: 25px;"><col style="min-width: 25px;"><col style="min-width: 25px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Code</p></th><th colspan="1" rowspan="1"><p>Reason</p></th><th colspan="1" rowspan="1"><p>Cause</p></th><th colspan="1" rowspan="1"><p>Action</p></th></tr><tr><td colspan="1" rowspan="1"><p><code>4009</code></p></td><td colspan="1" rowspan="1"><p><code>You got disconnected by the app.</code></p></td><td colspan="1" rowspan="1"><p>Takeover by a different <code>consumerId</code></p></td><td colspan="1" rowspan="1"><p>Stop; don't reconnect</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>4200</code></p></td><td colspan="1" rowspan="1"><p><code>Server shutting down</code></p></td><td colspan="1" rowspan="1"><p>A realtime node restarts (deploys, scaling)</p></td><td colspan="1" rowspan="1"><p>Reconnect immediately</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>4201</code></p></td><td colspan="1" rowspan="1"><p><code>Pong reply not received in time</code></p></td><td colspan="1" rowspan="1"><p>After 120 s without traffic the server sends <code>pusher:ping</code>; no <code>pusher:pong</code> within 30 s</p></td><td colspan="1" rowspan="1"><p>Answer pings (or send <code>pusher:ping</code> yourself every ~60 s); reconnect</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>4100</code></p></td><td colspan="1" rowspan="1"><p>—</p></td><td colspan="1" rowspan="1"><p>Over capacity or the client reads too slowly</p></td><td colspan="1" rowspan="1"><p>Reconnect with backoff</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>1001</code> / <code>1006</code> / no <code>pusher:error</code></p></td><td colspan="1" rowspan="1"><p>—</p></td><td colspan="1" rowspan="1"><p>Network path: edge proxy, load balancer, your own proxy or NAT idle timeouts</p></td><td colspan="1" rowspan="1"><p>Reconnect with backoff</p></td></tr></tbody></table>

There is no maximum connection lifetime. A failed channel authorization (for example a revoked token) does not close the socket; it arrives as `pusher:subscription_error`. In general the Pusher ranges apply: `4000`–`4099` fix the cause before reconnecting, `4100`–`4199` reconnect with backoff, `4200`–`4299` reconnect immediately.

## Resync on every (re)connect — mandatory

A dropped connection loses the events published during the gap; there is no replay. After **every** connect — first connect, reconnect, and after a takeover — run per-conversation catch-up for anything you track:

1.  `GET /v3/s2s/contacts/{userId}/conversations` for contacts with open activity, and/or
    
2.  `GET /v3/s2s/conversations/{id}/messages?after=<last message id you have>` per conversation.
    

This is the same self-healing model the Gleap widget uses, and it also covers webhook consumers after their retry window.

## Related

- [Overview](https://docs.gleap.ai/en/articles/718-overview.md)
- [Authentication](https://docs.gleap.ai/en/articles/719-authentication.md)
- [Quickstart](https://docs.gleap.ai/en/articles/720-quickstart.md)
- [Workflows & bots](https://docs.gleap.ai/en/articles/721-workflows-and-bots.md)
- [Webhooks & events](https://docs.gleap.ai/en/articles/722-webhooks-and-events.md)
