---
title: Pub/Sub at the edge with Fanout
summary: null
url: >-
  https://www.fastly.com/documentation/guides/concepts/real-time-messaging/fanout
---

Fanout is a publish/subscribe message broker built into the Fastly platform and designed to power real-time and streaming applications. It lets you deliver live updates—such as chat messages, data feeds, or event notifications—instantly to browsers, mobile apps, servers, and other clients.

Fanout is available to Fastly Compute services. When your Compute service receives a client request, your application can perform a Fanout handoff—an operation that tells Fanout to take over the connection while directing the request to a backend. Based on instructions included in the backend response, Fanout can then keep the connection open and subscribe the connection to one or more _channels_, which act like named topics for broadcasting data. Your backend can be an external origin or a Fastly service—even the same Compute service.

To send data to clients, you publish a message to a channel using the [Fastly Publishing API](https://www.fastly.com/documentation/reference/api/publishing/). Fanout distributes the message to all active connections subscribed to that channel. This allows you to transform a typical HTTP request into an event-driven stream using techniques like Server-Sent Events, Long-Polling, or WebSockets.

Fanout's architecture offers several advantages over proprietary streaming platforms:

- It runs at the Fastly edge, so any request pre-processing (like authentication) applies to your streaming traffic too.
- It works with standard HTTP clients and servers—including serverless functions.
- It allows you to turn any HTTP response into a real-time stream (e.g., streaming logs, progressive media, async results).
- No separate domains or streaming infrastructure are needed.
- It's built on the open [GRIP protocol](https://pushpin.org/docs/protocols/grip/) and powered by the open-source [Pushpin project](https://pushpin.org/).

> **HINT:** If you're looking for a completely plug-and-play solution for pub/sub, see our [Pub/Sub Application](https://github.com/fastly/pubsub).

## Enabling Fanout

### Enable Fanout for local development

Fastly's local development environment integrates with a local installation of [Pushpin](https://pushpin.org/), the open-source GRIP proxy server maintained by Fastly, to enable local testing of Fanout features in Compute applications.

To test Fanout features in a local development environment, you will need all of the following:

- [Fastly CLI](https://www.fastly.com/documentation/reference/tools/cli/) version 13.1.0 or newer
- [Viceroy](https://github.com/fastly/Viceroy) version 0.14.0 or newer (usually managed by Fastly CLI)
- [a local installation](https://pushpin.org/docs/install/) of Pushpin

To enable Fanout features for local testing, set the following in the application's `fastly.toml` file:

```toml
[local_server.pushpin]
enable = true
```

### Enable Fanout on your Fastly service

> **IMPORTANT:** Fanout [requires the purchase of a paid Fastly account](https://docs.fastly.com/products/fanout#prerequisites) with at least one Compute service created on it.

Fanout is an optional upgrade to Fastly service plans and is disabled by default. If you have not yet purchased access, [contact sales](mailto:sales@fastly.com). Alternatively, anyone assigned the [role of superuser or engineer](https://www.fastly.com/documentation/guides/account-info/user-and-account-management/about-user-roles-and-permissions) can enable a 30-day trial directly in the web interface in the service configuration settings of a Compute service, [via the API](https://www.fastly.com/documentation/reference/api/products/fanout/), or using the Fastly CLI:

```term
$ fastly products --enable=fanout
```

> **WARNING:** Enabling or disabling Fanout immediately impacts all service versions, including the active one.

## Quick start

There are many different ways of using Fanout, but to quickly see what it can do, use one of the fully-featured Compute starter kits:

- [Rust](https://www.fastly.com/documentation/solutions/starters/compute-starter-kit-rust-fanout/)
- [JavaScript](https://www.fastly.com/documentation/solutions/starters/compute-starter-kit-javascript-fanout/)
- [Go](https://www.fastly.com/documentation/solutions/starters/compute-starter-kit-go-fanout/)

### Create a project from template

To create a [Compute](https://www.fastly.com/documentation/guides/compute) project with Fanout pre-configured, use <kbd>fastly compute init</kbd>:

### Rust

```term
  $ fastly compute init --from=https://github.com/fastly/compute-starter-kit-rust-fanout
```

### Javascript

```term
  $ fastly compute init --from=https://github.com/fastly/compute-starter-kit-javascript-fanout
```

### Go

```term
  $ fastly compute init --from=https://github.com/fastly/compute-starter-kit-go-fanout
```

### Run the service locally

Fastly's local development environment integrates with a local installation of [Pushpin](https://pushpin.org/) to enable local testing of Fanout features in Compute applications.

> **NOTE:** Install Pushpin according to [the instructions for your environment](https://pushpin.org/docs/install/). See full [requirements](https://www.fastly.com/documentation/guides/concepts/real-time-messaging/fanout#enable-fanout-for-local-development) for testing Fanout features locally.

The starter kit project enables Fanout features for local testing by setting the following in the `fastly.toml` file:

```toml
[local_server.pushpin]
enable = true
```

Once the project has been initialized, run the project in your local development environment:

```term
fastly compute serve
```

> **HINT:** By default, the Fastly CLI searches your system path for `pushpin`. Alternatively, specify the path to `pushpin` when starting the test environment:
>
> ```term
> fastly compute serve --pushpin-path=/path/to/pushpin
> ```

Because Pushpin is enabled, you will see Pushpin initialization messages included in the startup output of the development server.

```plain
✓ Verifying fastly.toml
✓ Identifying package name
✓ Identifying toolchain
✓ Running [scripts.build]
✓ Creating package archive
SUCCESS: Built package (pkg/fanout-starter-kit.tar.gz)
✓ Running local Pushpin
2025-07-29T04:31:16.071246Z  INFO [Pushpin] using config: /opt/homebrew/etc/pushpin/pushpin.conf
2025-07-29T04:31:16.358935Z  INFO [Pushpin] starting connmgr
2025-07-29T04:31:16.360125Z  INFO [Pushpin] starting proxy
2025-07-29T04:31:16.360900Z  INFO [Pushpin] starting handler
✓ Running local server
```

#### Test locally

Once the service is running, it listens for HTTP requests at `localhost:7676`. The publishing API is available at `http://localhost:5561/publish/`.

In the starter kit project, requests with URLs beginning with `/test/` trigger a "Fanout handoff" [to itself](https://www.fastly.com/documentation/guides/concepts/real-time-messaging/fanout#negotiating-streams-at-the-edge) via a backend named `self`, defined in `fastly.toml`:

```toml
[local_server.backends]
[local_server.backends.self]
url = "http://localhost:7676/"
override_host = "localhost:7676"
```

Thus, the starter kit is invoked again, this time through Fanout (indicated by the request header `Grip-Sig` having a value). It creates long-lived connections for requests to `/test/stream`, `/test/sse`, `/test/long-poll`, and `/test/websocket`, subscribing clients to a channel called `test`.

You can now test the starter kit using any of the supported transports:

### HTTP Streaming (incl. Server-Sent Events)

  To test the starter kit using HTTP Streaming, in a terminal window, make an HTTP request for `/test/stream`:

```term
$ curl -i "http://localhost:7676/test/stream"
HTTP/1.1 200 OK
content-type: text/plain
date: Tue, 23 Aug 2022 12:48:05 GMT
connection: Transfer-Encoding
transfer-encoding: chunked
```

  You'll see output such as the above but you won't return to the shell prompt. Now, in another terminal window, run:

```term
$ curl -d '{"items":[{"channel":"test","formats":{"http-stream":{"content": "hello\n"}}}]}' http://localhost:5561/publish/
```

  The published data includes an `http-stream` representation of your data, which Fanout can use for streaming connections. The event you published appears on your curl output:

```term
hello
```

  You can continue to publish more messages, and they will be appended to the streaming response.

  [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) (SSE) is a specialized application of HTTP Streaming that uses the `Content-Type` value of `text/event-stream` on the response. To test the starter kit using SSE, in a terminal window, make an HTTP request for `/test/sse`:

```term
$ curl -i "http://localhost:7676/test/sse"
HTTP/1.1 200 OK
content-type: text/event-stream
date: Tue, 23 Aug 2022 12:48:05 GMT
connection: Transfer-Encoding
transfer-encoding: chunked
```

  Now, in another terminal window, run:

```term
$ curl -d '{"items":[{"channel":"test","formats":{"http-stream":{"content": "event: message\ndata: {\"text\": \"hello world\"}\n\n"}}}]}' http://localhost:5561/publish/
```

  The event you published appears on your curl output:

```term
event: message
data: {"text": "hello world"}

```

### HTTP Long polling

  To test the starter kit using HTTP Long polling, in a terminal window, make an HTTP request for `/test/long-poll`:

```term
$ curl -i "http://localhost:7676/test/long-poll"
```

  No output will appear - that's good, Fanout is holding the connection open. Now, in another terminal window, run:

```term
$ curl -d '{"items":[{"channel":"test","formats":{"http-response": {"body": "test 1"}}}]}' http://localhost:5561/publish/
```

  The published data includes an `http-response` representation of your data, which Fanout can use for long-polling connections. The event you published appears on your curl output, along with the headers:

```term
HTTP/1.1 200 OK
content-type: text/plain
date: Tue, 23 Aug 2022 13:05:29 GMT
content-length: 6

test 1
```

  Fanout also closes the connection at this point. Typically, a client would now make a new request, so that there is always a "hanging GET," ready for an event to occur on the server side.

### WebSockets

  To test the starter kit using a WebSockets client such as [`wscat`](https://www.npmjs.com/package/wscat) to open a WebSocket connection to your Fastly service, in a terminal window run:

```term
wscat -c ws://localhost:7676/test/websocket
Connected (press CTRL+C to quit)
>
```

  In another terminal window, run:

```term
$ curl -d '{"items":[{"channel":"test","formats":{"ws-message":{"content":"hello"}}}]}' http://localhost:5561/publish/
```

  The published data includes a `ws-message` representation of your data, which Fanout can use for WebSocket connections. The event you published appears on the WebSocket terminal:

```term
< hello
```

  Because WebSockets are bidirectional, you can also type into the WebSocket terminal and press ENTER to send your message to the server. The starter kit is configured to echo back the message you send it:

```term
> message from client
< message from client
```

The pattern implemented in the starter kit sets up streams entirely at the edge and works best when logic in the Compute service determines the channels for a connecting client. In this setup, the origin handles only [publishing](https://www.fastly.com/documentation/guides/concepts/real-time-messaging/fanout#publishing) events, while the Compute service [negotiates the setup of streams](https://www.fastly.com/documentation/guides/concepts/real-time-messaging/fanout#negotiating-streams-at-the-edge).

### Deploy to a Fastly service

You can compile and publish the starter kit project to a live Fastly service using <kbd>fastly compute publish</kbd>:

```term
$ fastly compute publish
Create new service: [y/N] y

Domain: [some-funky-words.edgecompute.app]
Backend (hostname or IP address, or leave blank to stop adding backends):

✓ Creating domain 'some-funky-words.edgecompute.app'...
✓ Uploading package...
✓ Activating version...

SUCCESS: Deployed package (service 0eBOC1x5Q0HHadAlpeKbvt, version 1)
```

#### Add a backend

Fanout communicates with a backend server to get instructions on what to do with each new connection. The starter kit is configured to [use itself](https://www.fastly.com/documentation/guides/concepts/real-time-messaging/fanout#negotiating-streams-at-the-edge) as the backend. To make use of this, add a backend called `self` to your service that directs requests back to the service itself:

```term
$ fastly backend create --name self -s {SERVICE_ID} --address {PUBLIC_DOMAIN} --port 443 --version latest --autoclone
$ fastly service-version activate --version latest -s {SERVICE_ID}
```

The `{SERVICE_ID}` and `{PUBLIC_DOMAIN}` should be replaced by the values shown in the output from the `publish` step.

> **IMPORTANT:** If you use the web interface or API to create the backend, ensure to set a **host header override** if your server's hosting is name-based. [Learn more](https://www.fastly.com/documentation/guides/integrations/non-fastly-services/developer-guide-backends/#creating-backends).

#### Enable Fanout

Fanout is an optional upgrade to Fastly service plans and is disabled by default.

> **NOTE:** See full [requirements](https://www.fastly.com/documentation/guides/concepts/real-time-messaging/fanout#enable-fanout-on-your-fastly-service) for enabling Fanout on your Fastly service.

Fanout can be enabled on an individual service in the web interface or by enabling the `fanout` product using the [product enablement API](https://www.fastly.com/documentation/reference/api/products/enablement/) or the Fastly CLI:

```term
$ fastly products --enable=fanout
```

> **WARNING:** Enabling or disabling Fanout immediately impacts all service versions, including the active one.

#### Authenticating

To make the API calls to perform publishing actions, you'll need a [Fastly API Token](https://www.fastly.com/documentation/reference/api/#authentication) that has the `global` scope for your service.

#### Test on Fastly

Once the starter kit is deployed to your Fastly service, you can now perform the same tests as in [Test locally](https://www.fastly.com/documentation/guides/concepts/real-time-messaging/fanout#test-locally) above, repeated below for convenience.

> **NOTE:** Channel names are scoped to the Fastly service.

### HTTP Streaming (incl. Server-Sent Events)

In one terminal window, make an HTTP request for `/test/stream`:

```term
$ curl -i "https://some-funky-words.edgecompute.app/test/stream"
HTTP/2 200
content-type: text/plain
x-served-by: cache-lhr7380-LHR
date: Tue, 23 Aug 2022 12:48:05 GMT
```

  You'll see output such as the above but you won't return to the shell prompt. Now, in another terminal window, run:

```term
$ curl -H "Fastly-Key: {YOUR_FASTLY_TOKEN}" -d '{"items":[{"channel":"test","formats":{"http-stream":{"content": "hello\n"}}}]}' https://api.fastly.com/service/{SERVICE_ID}/publish/
```

  The published data includes an `http-stream` representation of your data, which Fanout can use for streaming connections. The event you published appears on your curl output:

```term
hello
```

  You can continue to publish more messages, and they will be appended to the streaming response.

  [Server-Sent Events (SSE)](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) is a specialized application of HTTP Streaming that uses the `Content-Type` value of `text/event-stream` on the response.

  The starter kit provides a test endpoint for SSE. Make a HTTP request for `/test/sse`:

```term
$ curl -i "https://some-funky-words.edgecompute.app/test/sse"
HTTP/2 200
content-type: text/event-stream
x-served-by: cache-lhr7380-LHR
date: Tue, 23 Aug 2022 12:48:05 GMT
```

  Now, in another terminal window, run:

```term
$ curl -H "Fastly-Key: {YOUR_FASTLY_TOKEN}" -d '{"items":[{"channel":"test","formats":{"http-stream":{"content": "event: message\ndata: {\"text\": \"hello world\"}\n\n"}}}]}' https://api.fastly.com/service/{SERVICE_ID}/publish/
```

  The event you published appears on your curl output:

```term
event: message
data: {"text": "hello world"}

```

### HTTP Long polling

In one terminal window, make an HTTP request for `/test/long-poll`:

```term
$ curl -i "https://some-funky-words.edgecompute.app/test/long-poll"
```

  No output will appear - that's good, Fastly's edge network is holding the connection open. Now, in another terminal window, run:

```term
$ curl -H "Fastly-Key: {YOUR_FASTLY_TOKEN}" -d '{"items":[{"channel":"test","formats":{"http-response": {"body": "test 1"}}}]}' https://api.fastly.com/service/{SERVICE_ID}/publish/
```

  The published data includes an `http-response` representation of your data, which Fanout can use for long-polling connections. The event you published appears on your curl output, along with the headers:

```term
HTTP/2 200
content-type: text/plain
x-served-by: cache-lcy19274-LCY
date: Tue, 23 Aug 2022 13:05:29 GMT
content-length: 6

test 1
```

  Fanout also closes the connection at this point. Typically, a client would now make a new request, so that there is always a 'hanging GET', ready for an event to occur on the server side.

### WebSockets

Use a WebSockets client such as [`wscat`](https://www.npmjs.com/package/wscat) to open a WebSocket connection to your Fastly service:

```term
wscat -c wss://some-funky-words.edgecompute.app/test/websocket
Connected (press CTRL+C to quit)
>
```

  In another terminal window, run:

```term
$ curl -H "Fastly-Key: {YOUR_FASTLY_TOKEN}" -d '{"items":[{"channel":"test","formats":{"ws-message":{"content":"hello"}}}]}' https://api.fastly.com/service/{SERVICE_ID}/publish/
```

  The published data includes a `ws-message` representation of your data, which Fanout can use for WebSocket connections. The event you published appears on the WebSocket terminal:

```term
< hello
```

  Because WebSockets are bidirectional, you can also type into the WebSocket terminal and press ENTER to send your message to the server. The starter kit is configured to echo back the message you send it:

```term
> message from client
< message from client
```

### Next steps

Now you have an operational Fanout message broker. Consider how you might want to modify this setup to suit your needs:

- Learn more about [subscribing](https://www.fastly.com/documentation/guides/concepts/real-time-messaging/fanout#subscribing), including examples of the front-end JavaScript code you need to interact with streams.
- Learn more about [publishing](https://www.fastly.com/documentation/guides/concepts/real-time-messaging/fanout#publishing), including simple libraries that can abstract the complexity of message formatting for you.
- If you only need one kind of transport (e.g., WebSockets, and not SSE), feel free to remove the code that enables the other transports.
- If you prefer to have your origin server do the stream setup, then most of the edge code is no longer needed. See [Connection setup](https://www.fastly.com/documentation/guides/concepts/real-time-messaging/fanout#connection-setup) below.
- If you intend to use the new service in production, you'll want to add at least one origin server and a domain, and consider how you want the Fastly platform to cache your non-streamed content.

## Connection setup

Fanout connections are created by explicitly calling the appropriate **handoff** method in your preferred Compute language SDK. Fanout then queries a nominated backend to find out what to do with the request. It's up to the backend to tell Fanout to treat the request as a stream and to provide a list of channels that client should subscribe to.

![Workflow for connection setup](/img/setup-precomposed.svg)

The Compute application decides what kinds of requests to hand off to Fanout (3), and the backend application decides what channels to subscribe the client to (5). This backend can also be a Fastly service—it can even be the same service.

> **IMPORTANT:** Fanout handoff cannot specify a [dynamic backend](https://www.fastly.com/documentation/guides/integrations/non-fastly-services/developer-guide-backends/#dynamic-backends). An approximate workaround that can be used is to perform a handoff to the service itself, and then in that inner call, to forward the request to a dynamic backend.

### Responding to Fanout requests

Fanout communicates with backends by forwarding client requests and interpreting instructions in the response formatted using [Generic Realtime Intermediary Protocol (GRIP)](https://pushpin.org/docs/protocols/grip/). When a client request is handed off to Fanout, Fanout will forward the request to the backend specified in the handoff. The backend response can tell Fanout how to handle the connection lifecycle, using GRIP instructions.

### HTTP Streaming (incl. Server-Sent Events)

Fanout forwards regular HTTP requests to the backend. Client request headers that are added, removed, or modified on your Request will be reflected in the Fanout handoff.

  If the backend wants to use the request for HTTP streaming (including Server-Sent Events), it should use [GRIP](https://pushpin.org/docs/protocols/grip/) headers to instruct Fanout to hold that response as a stream:

```http
HTTP/1.1 200 OK
Content-Type: text/plain
Grip-Hold: stream
Grip-Channel: mychannel
Grip-Channel: anotherchannel
```

  The GRIP headers that are relevant to initiating HTTP streams are:

- `Grip-Hold`: Set to `stream` to tell Fanout to deliver the headers of the response to the client immediately, and then deliver messages as they are [published](https://www.fastly.com/documentation/guides/concepts/real-time-messaging/fanout#publishing) to subscribed channels.
- `Grip-Channel`: A channel to subscribe the request to. Multiple Grip-Channel headers may be specified in the response, to subscribe multiple channels to the request.
- `Grip-Keep-Alive`: Data to be sent to the client after a certain amount of activity passes. The `timeout` parameter specifies the length of time a request must be idle before the keep alive data is sent (default 55 seconds). The `format` parameter specifies the format of the keep alive data. Allowed values are `raw`, `cstring`, and `base64` (default `raw`). For example, if a newline character should be sent to the client after 20 seconds of inactivity, the following header could be used: `Grip-Keep-Alive: \n; format=cstring; timeout=20`.

  Messages to be published to a client in a `Grip-Hold: stream` state must have an `http-stream` format available. [Learn more about publishing](https://www.fastly.com/documentation/guides/concepts/real-time-messaging/fanout#publishing).

  [Server-Sent Events (SSE)](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) is a specialized application of HTTP Streaming that uses the `Content-Type` value of `text/event-stream` on the response.

```http
HTTP/1.1 200 OK
Content-Type: text/event-stream
Grip-Hold: stream
Grip-Channel: mychannel
```

  Compliant Server-sent events clients (such as the `EventSource` API built into web browsers) will send a `Last-Event-ID` header with new connection requests. If you care about ensuring clients do not miss events during reconnects, consider parsing this header and including missed events in the initial response along with the `Grip-Hold` header, allowing subsequent events provided via the [publishing](https://www.fastly.com/documentation/guides/concepts/real-time-messaging/fanout#publishing) API to be appended by Fanout later to the same response.

### HTTP Long polling

Fanout forwards regular HTTP requests to the backend unmodified. Client request headers that are added, removed, or modified on your Request will be reflected in the Fanout handoff.

  If the backend wants to use the request for long polling, it should use [GRIP](https://pushpin.org/docs/protocols/grip/) headers to instruct Fanout to hold that response and wait for an alternative response via the [publishing](https://www.fastly.com/documentation/guides/concepts/real-time-messaging/fanout#publishing) API.

```http
HTTP/1.1 200 OK
Content-Type: application/json
Grip-Hold: response
Grip-Channel: mychannel

{}
```

  Following this response to Fanout, the connection with the backend ends, but the client's connection with the Fastly edge remains, and the Fastly platform does not issue any response data. If a message is published on `mychannel` within the GRIP timeout period, the response is modified to use the published message as the body, and then delivered to the client and the connection ends. Otherwise, the response is delivered to the client unmodified at the end of the timeout period.

> **WARNING:** If a large number of clients are connected to a small number of channels using long polling, be aware of creating very sudden inrushes of requests after a message is published.

  The GRIP headers that are relevant to setting up long-poll responses are:

- `Grip-Hold`: Set to `response` to tell Fanout to wait for a message to be [published](https://www.fastly.com/documentation/guides/concepts/real-time-messaging/fanout#publishing) to subscribed channels. As soon as a message is published, it is delivered to the client and the response ends.
- `Grip-Channel`: A channel to subscribe the request to. Multiple Grip-Channel headers may be specified in the response, to subscribe multiple channels to the request. An optional `prev-id` parameter may be used to specify the ID of data that was last published to the specified channel (used to avoid a race condition).
- `Grip-Timeout`: The length of time the request should be held open before timing out (default 55 seconds).

  Messages to be published to a client in a `Grip-Hold: response` state must have an `http-response` format available. [Learn more about publishing](https://www.fastly.com/documentation/guides/concepts/real-time-messaging/fanout#publishing).

  Since long polling clients spend significant periods reconnecting, you may want to include a 'last event ID' in the client request (e.g. in a query parameter: `/test/stream?last-event=123`). When the origin receives the request, it can check whether any events have occurred since the specified event, and if so return them with no GRIP headers, which will prompt Fanout to deliver the response to the client immediately. The client would then immediately begin a new request citing the new last event ID.

### WebSockets

When Fanout forwards a WebSocket request to a backend, it converts the request to a `POST` request using [WebSockets-over-HTTP](https://pushpin.org/docs/protocols/websocket-over-http/), and adds a `Sec-WebSocket-Extensions: grip` header. Such a request may look like this:

```http
POST /original/request/path HTTP/1.1
Sec-WebSocket-Extensions: grip
Content-Type: application/websocket-events
Accept: application/websocket-events

OPEN\r\n
```

  The server should reply with a `200` (OK) response, and echo the `OPEN` command, along with any [GRIP control messages](https://pushpin.org/docs/protocols/grip/) it wants to send. Typically, this is an opportunity to indicate which channels the client should subscribe to:

```http
HTTP/1.1 200 OK
Sec-WebSocket-Extensions: grip
Content-Type: application/websocket-events

OPEN\r\n
TEXT 2F\r\n
c:{"type": "subscribe", "channel": "mychannel"}\r\n
```

  This example shows two [WebSockets-over-HTTP](https://pushpin.org/docs/protocols/websocket-over-http/) messages, the second of which is a TEXT message that contains a GRIP subscribe command.

  At this point the WebSocket connection is established between the client and the Fastly edge, and any messages that are [published](https://www.fastly.com/documentation/guides/concepts/real-time-messaging/fanout#publishing) to the subscribed channels will be sent to the client over that WebSocket connection. Any additional messages sent from the client over this WebSocket connection will trigger a new [WebSockets-over-HTTP](https://pushpin.org/docs/protocols/websocket-over-http/) request from Fanout to the backend. See [inbound WebSockets messages](https://www.fastly.com/documentation/guides/concepts/real-time-messaging/fanout#inbound-websockets-messages) to learn more.

### Validating GRIP requests

Before responding with GRIP instructions, it's a best practice for your backend code to check that the request is actually coming through Fastly Fanout and that this instance of Fanout is associated with your service. This can be done by examining the `Grip-Sig` header value set by Fastly Fanout as it sends a request to a backend.

`Grip-Sig` is provided as a [JSON Web Token](https://www.jwt.io) and should be validated and verified using an appropriate library for the language platform. For example, if your backend uses JavaScript, use a library such as [`jose`](https://github.com/panva/jose).

Tokens signed by Fastly Fanout can be validated using the following public key.

- **PEM format**
  ```text
  -----BEGIN PUBLIC KEY-----
  MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAECKo5A1ebyFcnmVV8SE5On+8G81Jy
  BjSvcrx4VLetWCjuDAmppTo3xM/zz763COTCgHfp/6lPdCyYjjqc+GM7sw==
  -----END PUBLIC KEY-----
  ```

- **JSON Web Key (JWK) format**
  ```json
  {
    "kty": "EC",
    "crv": "P-256",
    "x": "CKo5A1ebyFcnmVV8SE5On-8G81JyBjSvcrx4VLetWCg",
    "y": "7gwJqaU6N8TP88--twjkwoB36f-pT3QsmI46nPhjO7M"
  }
  ```
  > **NOTE:**
  > If your backend is running on JavaScript on Fastly Compute, the runtime does not support PEM-formatted keys at this time. Use the JWK format instead.

Additionally, the following must be verified:

- The token's `iss` (issuer) claim matches the following value, where `<service-id>` is the service ID of the service that performs the Fanout handoff:
  ```plain
  fastly:<service-id>
  ```
- The token's expiration time has not elapsed.

If the token cannot be fully validated or verified for any reason, then the backend is expected to behave as though the header isn't present.

> **HINT:**
> When using the [local development environment](https://www.fastly.com/documentation/guides/concepts/real-time-messaging/fanout#local-testing) to test
> Fanout features locally, use the following symmetric key to validate
> the `Grip-Sig`.
>
> ```plain
> viceroy_dev_signing_key_dont_use_in_production
> ```
>
> In addition, use the following `iss` value:
>
> ```plain
> viceroy
> ```

#### Example

This example validates that the `Grip-Sig` header value is signed by Fastly Fanout and verifies its issuer.

```javascript

const FANOUT_PUBLIC_KEY = {
  "kty": "EC",
  "crv": "P-256",
  "x": "CKo5A1ebyFcnmVV8SE5On-8G81JyBjSvcrx4VLetWCg",
  "y": "7gwJqaU6N8TP88--twjkwoB36f-pT3QsmI46nPhjO7M"
};

const publicKey = await crypto.subtle.importKey(
  'jwk', FANOUT_PUBLIC_KEY, { name: 'ECDSA', namedCurve: 'P-256' },
  false, ['verify']);
const result = await jose.jwtVerify(req.headers.get('Grip-Sig'), publicKey, { issuer: 'fastly:xxxxxxxxxxxxxxxxxxxxxx' });
```

### What to hand off to Fanout

<div id="what-to-upgrade"></div>

You should be selective about requests you hand off to Fanout (e.g., by checking its URL path or headers). Handing off a request that isn't intended to be a stream may still work, because Fanout will relay that request to origin, and if the response is not GRIP or WebSocket-over-HTTP, Fanout will simply relay it back to the client and close the connection. However, passing all requests through Fanout is not recommended, for a number of reasons:

- Only limited changes to requests (e.g., to request headers) will be reflected when handing off to Fanout.
- The request will not interact with the Fastly cache, so even content that is not intended to be streamed will not be cached.
- Responses from origin will be delivered directly to the client by Fanout, and will not be available to the Compute program.

As a result it usually makes sense to hand off requests only when they target a known path or set of paths on which you want to stream responses:

### Rust

```rust
use fastly::{Error, Request};

fn main() -> Result<(), Error> {
    let req = Request::from_client();

    if req.get_path().starts_with("/test/") {
        // Hand off stream requests to the stream backend
        return Ok(req.handoff_fanout("stream_backend")?)
    }

    // Forward all non-stream requests to the primary backend
    Ok(req.send("primary_backend")?.send_to_client())
}
```

### Javascript

```javascript

async function handleRequest(event) {
  const url = new URL(event.request.url);
  if (url.pathname.startsWith('/test/')) {
    // Hand off stream requests to the stream backend
    return createFanoutHandoff(event.request, 'stream_backend');
  }

  // Forward all non-stream requests to the primary backend
  return fetch(event.request, { backend: 'primary_backend' });
}

addEventListener("fetch", (event) => event.respondWith(handleRequest(event)));
```

### Go

```go
package main

import (
  "context"
  "io"
  "strings"

  "github.com/fastly/compute-sdk-go/fsthttp"
  "github.com/fastly/compute-sdk-go/x/exp/handoff"
)

func main() {
  fsthttp.ServeFunc(func(ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request) {
    if strings.HasPrefix(r.URL.Path, "/test/") {
      // Hand off stream requests to the stream backend
      handoff.Fanout("stream_backend")
      return
    }

    // Forward all non-stream requests to the primary backend
    resp, _ := r.Send(ctx, "primary_backend")
    w.Header().Reset(resp.Header)
    w.WriteHeader(resp.StatusCode)
    io.Copy(w, resp.Body)
  })
}
```

### Negotiating streams at the edge

<div id="using-fastly-as-a-fanout-backend"></div>
<div id="using-a-fastly-compute-service-as-a-fanout-backend"></div>

Fanout separates the concerns of subscribing and publishing. This makes it possible to negotiate streams at the edge, potentially reducing latency, simplifying infrastructure, and improving flexibility in how streaming endpoints are scaled and secured.

To do this, configure the Fastly Compute service itself as the Fanout backend. The Fanout starter kits demonstrate this pattern by creating a backend named `self` that points to the public domain name of the same Compute service, and referencing that backend when handing off to Fanout. In this configuration, the Compute program both handles the initial client request and responds to the stream negotiation request from Fanout.

This approach is suitable when:

- The number of channels is relatively small
- Clients explicitly specify which channels to subscribe to
- The publisher does not require visibility into subscriber state

In the following cases, this approach would not be suitable, and stream negotiation should be performed by your origin server:

- Subscriber state must be visible at the origin
- Stream authentication or long-polling is only implemented at the origin
- Stream determination cannot be made in advance
- Clients must not miss messages during reconnects

When Fanout relays requests to the backend, it preserves the original path and adds a `Grip-Sig` header. These can be used together to distinguish streaming-related traffic and determine whether a request is relayed by Fanout or sent directly from a client.

### Rust

```rust
use fastly::{Error, Request, Response};
use fastly::http::StatusCode;

fn handle_fanout(req: Request, chan: &str) -> Response {
    Response::from_status(StatusCode::OK)
}

fn main() -> Result<(), Error> {
    let req = Request::from_client();

    // Request is a stream request
    if req.get_path().starts_with("/test/") {
        if req.get_header_str("Grip-Sig").is_some() {
            // Request is from Fanout
            return Ok(handle_fanout(req, "test").send_to_client());
        }

        // Not from Fanout, route back to self through Fanout first
        return Ok(req.handoff_fanout("self")?);
    }

    // Forward all non-stream requests to the primary backend
    Ok(req.send("primary_backend")?.send_to_client())
}
```

### Javascript

```javascript

async function handleRequest(event) {
  const url = new URL(event.request.url);
  if (url.pathname.startsWith('/test/')) {
    if (event.request.headers.has('Grip-Sig')) {
      // Request is from Fanout
      return handleFanout(event.request, "test");
    }

    // Not from Fanout, route back to self through Fanout first
    return createFanoutHandoff(event.request, 'self');
  }

  // Forward all non-stream requests to the primary backend
  return fetch(event.request, { backend: 'primary_backend' });
}

addEventListener("fetch", (event) => event.respondWith(handleRequest(event)));
```

### Go

```go
package main

import (
  "context"
  "io"
  "strings"

  "github.com/fastly/compute-sdk-go/fsthttp"
  "github.com/fastly/compute-sdk-go/x/exp/handoff"
)

func main() {
  fsthttp.ServeFunc(func(ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request) {
    if strings.HasPrefix(r.URL.Path, "/test/") {
      if r.Header.Get("Grip-Sig") != "" {
        // Request is from Fanout, handle it here
        handleFanout(w, r, "test")
        return
      }

      // Not from Fanout, route back to self through Fanout first
      handoff.Fanout("self")
      return
    }

    // Forward all non-stream requests to the primary backend
    resp, _ := r.Send(ctx, "primary_backend")
    w.Header().Reset(resp.Header)
    w.WriteHeader(resp.StatusCode)
    io.Copy(w, resp.Body)
  })
}
```

The `handle_fanout` function (`handleFanout` in JavaScript and Go) invoked in the example above is a user-provided function that should return a GRIP HTTP or a WebSockets-over-HTTP response. The Fanout starter kits contain example implementations.

## Subscribing

Fanout is designed to allow push messaging to integrate seamlessly into your domain. When clients make HTTP requests or WebSocket connections that arrive at Fastly's network, what happens next depends on the instructions provided by your backend. These instructions can include subscribing the client to one or more channels.

For HTTP-based transports (such as Server-Sent Events and long polling), this is done with response headers. For example:

```http
Grip-Hold: stream
Grip-Channel: mychannel
```

For the WebSocket transport, this is done by sending GRIP control messages as part of a WebSockets-over-HTTP response. For example:

```http
c:{"type": "subscribe", "channel": "mychannel"}
```

It's important to understand that clients don't assert their own subscriptions. Clients make arbitrary HTTP requests or send arbitrary WebSocket messages, and it is your backend that determines whether clients should be subscribed to anything. Your channel schema remains private between Fanout and your backend server, and in fact clients may not even be aware that publish-subscribe activities are occurring.

> **HINT:** Your application's design may still allow for client requests to specify channel names. A path such as `/stream/departure-KR4N81` to get a real time stream of departure status for a flight booking, for example, is passing the name of the desired channel in the path. If the backend deems the client to be entitled to data from that channel, it could extract this token from the path and pass it to Fanout in a GRIP subscription instruction.

If your client is a web browser, you will use JavaScript to initiate streaming requests to the backend:

### HTTP Streaming (incl. Server-Sent Events)

Modern web browsers have support for reading from streams via the [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch) function and [ReadableStream interface](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream):

```javascript
const eventList = document.querySelector('ul');

const response = await fetch('/test/stream');
const reader = response.body.getReader();
while (true) {
  const { done, value } = await reader.read();
  if (done) {
    return;
  }

  const newElement = document.createElement("li");
  newElement.textContent = value;
  eventList.appendChild(newElement);
}
```

  Web browsers have built-in support for Server-Sent Events via the [EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource) API:

```javascript
const evtSource = new EventSource('/test/sse');
const eventList = document.querySelector('ul');

evtSource.onmessage = (event) => {
  const newElement = document.createElement("li");
  newElement.textContent = `message: ${event.data}`;
  eventList.appendChild(newElement);
};
```

  If your SSE events include an `id:` property, the EventSource will add a `Last-Event-ID` header to each request, which can be used to deliver missed messages when a new stream begins.

### HTTP Long polling

In long polling, the client need not do anything different to a normal HTTP request, since each response includes only one stream event. However, since the client loses its connection to the server after each event, it's necessary to send a new request to start listening for the next event. This frequent reconnection means events may be published while the client is not connected to the server. Consider sending a last-event ID with each request.

```javascript
const eventList = document.querySelector('ul');
let lastEventID;
while (true) {
  const lastEventParam = lastEventID ? `?last-event-id=${lastEventID}` : '';
  const resp = await fetch(`/test/long-poll${lastEventParam}`);
  if (!resp.ok) throw new Error(`HTTP ${resp.status} in stream response`);
  const items = await resp.json();
  items.forEach(({id, message}) => {
    const newElement = document.createElement("li");
    newElement.textContent = `message: ${message}`;
    eventList.appendChild(newElement);
    lastEventID = Math.max(lastEventID, id);
  });
}
```

### WebSockets

In order to communicate using the WebSocket protocol, you need to create a [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) object; this will automatically attempt to open the connection to the server. Unlike HTTP request based transports, WebSocket connections also allow the client to send data to the server (see [inbound WebSockets messages](https://www.fastly.com/documentation/guides/concepts/real-time-messaging/fanout#inbound-websockets-messages)).

```javascript
const ws = new WebSocket('/test/websocket');
const eventList = document.querySelector('ul');

ws.onmessage = (event) => {
  const newElement = document.createElement("li");
  newElement.textContent = `message: ${event.data}`;
  eventList.appendChild(newElement);
};

function sendMessage(text) {
  ws.send(text);
}
```

## Publishing

Messages are published to Fanout channels using the [publishing API](https://www.fastly.com/documentation/reference/api/publishing/). To publish events, send an HTTP POST request to `https://api.fastly.com/service/{SERVICE_ID}/publish/`. You'll need to [authenticate with a Fastly API Token](https://www.fastly.com/documentation/reference/api/#authentication) that has the `global` scope for your service.

Messages can also be delivered during [connection setup](https://www.fastly.com/documentation/guides/concepts/real-time-messaging/fanout#connection-setup) (often to provide events that the client missed while not connected), and in response to [inbound WebSocket messages](https://www.fastly.com/documentation/guides/concepts/real-time-messaging/fanout#inbound-websockets-messages). Events delivered in this way go to the client making the request (or sending the inbound WebSocket message), and do not use pub/sub channel subscriptions.

> **IMPORTANT:** Unlike other Fastly APIs, the publishing endpoint requires a trailing slash: `publish/`.

Publish requests include the messages to be published in a JSON data model:

| Property                                          | Type   | Description                                                                                                                                                                         |
| ------------------------------------------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `items`                                           | Array  | A list of messages to publish                                                                                                                                                       |
| `└─ [i]`                                          | Object | Each member of the array is a single message                                                                                                                                        |
| `   └─ id`                                        | String | A string identifier for the message. See [de-duplicatioon](https://www.fastly.com/documentation/guides/concepts/real-time-messaging/fanout#de-duplication).                         |
| `   └─ prev-id`                                   | String | Identifier of the previous message that was published to the channel. See [sequencing](https://www.fastly.com/documentation/guides/concepts/real-time-messaging/fanout#sequencing). |
| `   └─ channel`                                   | String | The name of the Fanout channel to which to publish the message. One channel per message.                                                                                            |
| `   └─ formats`                                   | Object | A set of representations of the message, suitable for different transports.                                                                                                         |
| `      └─ ws-message`                             | Object | A message representation suitable for delivery to WebSockets clients.                                                                                                               |
| `         └─ content`                             | String | Content of the WebSocket message.                                                                                                                                                   |
| <nobr><code>         └─ content-bin</code></nobr> | String | Base-64 encoded content of the WebSocket message (use instead of `content` if the message is not a string).                                                                         |
| `         └─ action`                              | String | A [publish action](https://www.fastly.com/documentation/guides/concepts/real-time-messaging/fanout#publish-actions).                                                                |
| `      └─ http-stream`                            | Object | A message representation suitable for delivery to Server-Sent events clients.                                                                                                       |
| `         └─ content`                             | String | Content of the SSE message. Must be compatible with the `text/event-stream` format.                                                                                                 |
| <nobr><code>         └─ content-bin</code></nobr> | String | Base-64 encoded content of the SSE message (use instead of `content` if the message is not a string).                                                                               |
| `         └─ action`                              | String | A [publish action](https://www.fastly.com/documentation/guides/concepts/real-time-messaging/fanout#publish-actions).                                                                |
| `      └─ http-response`                          | Object | A message representation suitable for delivery to Long-polling clients.                                                                                                             |
| `         └─ action`                              | String | A [publish action](https://www.fastly.com/documentation/guides/concepts/real-time-messaging/fanout#publish-actions).                                                                |
| `         └─ code`                                | Number | HTTP status code to apply to the response.                                                                                                                                          |
| `         └─ reason`                              | String | Informational label for HTTP status code (delivered only over HTTP/1.1)                                                                                                             |
| `         └─ headers`                             | Object | A key-value map of headers to set on the response.                                                                                                                                  |
| `         └─ body`                                | String | Complete body of the HTTP response to deliver.                                                                                                                                      |
| <nobr><code>         └─ body-bin</code></nobr>    | String | Base-64 encoded body content (use instead of `content` if the body is not a string).                                                                                                |

Minimally, a publish request must contain one message in at least one format, with the `content` property (for `http-stream` or `ws-message`) or the `body` property (for `http-response` specified). An example of a valid publish payload is:

```json
{
  "items": [
    {
      "channel": "test",
      "formats": {
        "ws-message": {
          "content": "hello"
        }
      }
    }
  ]
}
```

This can be sent using curl as shown:

```term
$ curl -H "Fastly-Key: {YOUR_FASTLY_TOKEN}" -d '{"items":[{"channel":"test","formats":{"ws-message":{"content":"hello"}}}]}' https://api.fastly.com/service/{SERVICE_ID}/publish/
```

> **WARNING:** If you are migrating to Fastly Fanout from self-hosted [Pushpin](https://pushpin.org/) or [fanout.io](https://fanout.io/), you may be using a [GRIP library](https://www.fastly.com/documentation/guides/concepts/real-time-messaging/fanout#libraries-and-sdks) in your server application. Some of these libraries currently are not compatible with Fastly Fanout.
> See [Libraries and SDKs](https://www.fastly.com/documentation/guides/concepts/real-time-messaging/fanout#libraries-and-sdks) for a list of libraries that are compatible with Fastly Fanout.

### Publish actions

Published items can optionally specify one of three actions:

- `send`: The included content should be delivered to subscribers. This is the default if unspecified.
- `hint`: The content to be delivered to subscribers must be externally retrieved. No content is included in the published item.
- `close`: The request or connection associated with the subscription should be ended/closed.

### Sequencing

If Fanout receives a message with a `prev-id` that doesn't match the `id` of an earlier message, then Fanout will buffer it until we receive a message whose `id` matches the value, at which point both messages will be delivered in the right order. If the expected message is never received, the buffered message will eventually be delivered anyway (around 5-10 seconds later).

### De-duplication

If Fanout receives a message with an `id` that we've seen already recently (within the last few seconds) the publish action will be accepted but no message will be created. This happens even if the message content is different from any prior messages which had the same `id`.

This feature is typically useful if you have an architecture with redundant publish paths. For example, you could have two publisher processes handling event triggers and have both send each message for high availability. Fanout would receive every message twice, but only process each message once. If one of the publishers fails, messages would still be received from the other.

### Limits

By default, messages are limited to 65,536 bytes for the “content” portion of the format being published. For the normal HTTP and WebSocket transports, the content size is the number of HTTP body bytes or WebSocket message bytes (TEXT frames converted to UTF-8).

## Inbound WebSockets messages

Unlike HTTP-based push messaging (e.g. server-sent events), WebSockets is bidirectional. When clients send messages to Fastly over an already-established WebSocket, Fanout will make a WebSockets-over-HTTP request to the Fanout backend, with a `TEXT` or `BINARY` segment containing the message from the client.

```http
POST /test/websocket HTTP/1.1
Sec-WebSocket-Extensions: grip
Content-Type: application/websocket-events
Accept: application/websocket-events

TEXT 16\r\n
Hello from the client!\r\n
```

The response from the backend may include `TEXT` or `BINARY` segments, which will be delivered to the client that sent the message (disregarding the channel-based pub/sub brokering). `TEXT` segments may also include GRIP control messages to instruct Fanout to modify the client stream, for example to change which channels it subscribes to.

```http
HTTP/1.1 200 OK
Content-Type: application/websocket-events

TEXT 0C\r\n
You said Hi!
TEXT 45\r\n
c:{"type": "subscribe", "channel": "additional-channel-subscription"}\r\n
```

The starter kits for Fanout include an example of handling inbound WebSockets messages by echoing the content of the message back to the client.

## Local Testing

Fastly's local development environment integrates with a local installation of [Pushpin](https://pushpin.org/) to enable local testing of Fanout features in Compute applications.

> **NOTE:** Install Pushpin according to [the instructions for your environment](https://pushpin.org/docs/install/). See full [requirements](https://www.fastly.com/documentation/guides/concepts/real-time-messaging/fanout#enable-fanout-for-local-development) for testing Fanout features locally.

To test Fanout features in the local development environment, include the `pushpin` section in your project's `fastly.toml` file:

```toml
[local_server.pushpin]
enable = true
```

By default, the Fastly CLI searches your system path for `pushpin`. See the [fastly.toml reference](https://www.fastly.com/documentation/reference/compute/fastly-toml#pushpin) for details on overriding this value.

In addition, set up any backends using the standard `backends` section. For example:

```toml
[local_server.backends]
[local_server.backends.origin]
url = "http://localhost:3000/"
override_host = "localhost:3000"
```

When you run `fastly compute serve`, the Fastly CLI configures Pushpin routes based on the defined backends and then starts Pushpin alongside your Compute application, enabling it for "Fanout handoff".

The publishing API is available at `http://localhost:5561/publish/`. See the [fastly.toml reference](https://www.fastly.com/documentation/reference/compute/fastly-toml#pushpin) for details on overriding this value.

> **IMPORTANT:** Specifying remote backends when testing Fanout handoff in the local development environment is valid. However, because the publishing API runs locally, it is often convenient to also locally run any backends or applications that need to test publishing.

## Libraries and SDKs

Libraries exist for many languages and frameworks to make it easier to interact with [GRIP](https://pushpin.org/docs/protocols/grip/). This includes common activities such as [initializing streams](https://www.fastly.com/documentation/guides/concepts/real-time-messaging/fanout#connection-setup), [publishing](https://www.fastly.com/documentation/guides/concepts/real-time-messaging/fanout#publishing), and [parsing WebSocket-over-HTTP messages](https://www.fastly.com/documentation/guides/concepts/real-time-messaging/fanout#inbound-websockets-messages).

The following GRIP libraries are compatible with Fastly Fanout. See the instructions of each library for details on usage with Fastly Fanout.

- Python: [`gripcontrol`](https://github.com/fanout/pygripcontrol), [django-grip](https://github.com/fanout/django-grip), [django-eventstream](https://github.com/fanout/django-eventstream)
- PHP: [`fanout/grip`](https://github.com/fanout/php-grip), [laravel-grip](https://github.com/fanout/laravel-grip)
- JavaScript: [`@fanoutio/grip`](https://github.com/fanout/js-grip), [@fanoutio/serve-grip](https://github.com/fanout/js-serve-grip), [@fanoutio/eventstream](https://github.com/fanout/js-eventstream)

### The GRIP_URL

These libraries are often configured using a `GRIP_URL`, which encodes the publishing endpoint and credentials for request validation and publishing.

It is usually set using environment variables, or in Compute, using the Secret Store, and is a recommended best practice for separating configuration between the local testing environment and your Fastly service.

Local testing:

- `GRIP_KEY` - Use: `http://127.0.0.1:5561/`

Fastly service:

- `GRIP_URL` - Use: `https://api.fastly.com/service/<service-id>/?verify-iss=fastly:<service-id>&key=<fastly-api-token>`
- `GRIP_VERIFY_KEY` - Use the following value:
  ```text
  base64:LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFQ0tvNUExZWJ5RmNubVZWOFNFNU9uKzhHODFKeQpCalN2Y3J4NFZMZXRXQ2p1REFtcHBUbzN4TS96ejc2M0NPVENnSGZwLzZsUGRDeVlqanFjK0dNN3N3PT0KLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0t
  ```
   This is a string used to configure the `verify-key` component of `GRIP_URL`, as it can get very long.

## Best practices

To get the most out of using Fanout on Fastly, consider the following tips:

- **Avoid stateful protocol designs**: for example keeping a client's last received message position in the server instead of in the client. These patterns will work, but they will be hard to reason about. It's best if the client asserts its own state.
- **Don't keep track of connections on the server**: Very rarely is it important to know about connections, rather than users. If you're implementing presence detection, better to do at the user/device level using heartbeats independently of connections.
