# Realtime

A WebSocket server for Laravel broadcasting next to your application, in your own Scaleway project. Browsers connect to it directly.


> [!NOTE]
> Realtime is in beta. It works and is measured on Scaleway, but its limits and defaults may still change. Running it on the Brewless image is a closed beta on top of that.

Realtime gives an environment a WebSocket server for Laravel broadcasting. The server is Laravel Reverb, and it runs in a Serverless Container called `realtime` next to your web, release and worker containers, in the same Scaleway project and region. Browsers connect to that container over `wss://`. Neither Brewless nor bunny.net sits in between.

## Switching it on

Add `realtime` to an environment in `brewless.yml` and deploy:

```yaml brewless.yml
environments:
  production:
    realtime: true
```

`true` means a realtime server on your application's own image, with every setting at its default. You can also set each one yourself:

```yaml brewless.yml
environments:
  staging:
    realtime:
      image: app
      memory: 512
      min: 0
      timeout: 3600
      host: true
```

| Setting | Default | What it does |
| --- | --- | --- |
| `image` | `app` | `app` runs Reverb on your application's release image. `brewless` runs it on a small image that Brewless maintains (closed beta, see below). |
| `memory` | `512` for `app`, `256` for `brewless` | Megabytes. If you leave out `cpu`, it follows the memory as it does for your other containers: 280 thousandths of a vCPU for 512 MB, 140 for 256 MB. |
| `cpu` | follows the memory | Thousandths of a vCPU. Memory and processor have to fit together, as on the other containers. |
| `min` | `0` | `0` lets the container sleep while nobody is connected. `1` keeps it running. One replica always holds every channel, so 1 is the highest value. |
| `timeout` | `3600` | How many seconds a connection may stay open, from 10 to 3600. |
| `host` | `true` | `true` puts the address at `ws.` plus your domain. `false` uses only the container's own address. A hostname such as `realtime.example.eu` uses that name. See [The address](#the-address). |

`realtime: false`, or no `realtime` key at all, means no realtime server. Every key and its bounds are listed in the [brewless.yml reference](/reference/brewless-yml).

## Two images

**Your application's image (`image: app`)** is generally available. The realtime container runs the image of the release with the arguments `reverb:start --host=0.0.0.0 --port=8080`. It gets every variable your application gets, so it can reach what your application reaches. When the environment is on a Private Network, the realtime container joins it too. It goes along with every deploy and every rollback, and it needs `laravel/reverb` in your `composer.json`.

**The Brewless image (`image: brewless`)** is in closed beta. This is `ghcr.io/brewless/brewless-realtime`: a bare Laravel skeleton with `laravel/reverb` and nothing of yours. Brewless pins it by digest, so a tag that moves later changes nothing. It gets only the Reverb keys and `REVERB_ALLOWED_ORIGINS`. That variable lists the host of your application's address and every domain under `domains` in `brewless.yml`, or every origin when neither names one. None of your other variables reach it. Every deploy puts it on the version Brewless pins, whatever your application changed. It starts at 256 MB.

### Requesting the beta

Anyone who may manage applications in your organisation can request it. In the console, open the environment's **Realtime** page and choose **Request access** under **Brewless image**. You can add a note on what you would run on it. The request goes to Brewless, and once a Brewless administrator turns the beta on, whoever requested it gets an email. Requesting it again changes nothing.

- **While it is off**, `brewless.yml` with `image: brewless` is refused when the file is checked, before anything is built. The deploy step refuses it as well.
- **When it is turned on**, the next deploy with `image: brewless` goes ahead.
- **When it is turned off again**, a realtime server that runs on the Brewless image keeps running. Brewless removes nothing of yours on its own. The next deploy with `image: brewless` is refused. Switch to `image: app`, or remove `realtime`, and deploy.

## What a deploy does

With `realtime` in `brewless.yml`, a deploy has three more steps. In the console they are called **Realtime server prepared**, **Realtime server** and **Realtime address**.

1. **Realtime server prepared**, before the build. The first time, this step makes the keys of the Reverb app: an app id, a key and a secret, kept as one secret named `<application>-<environment>-realtime` in your own Secret Manager. Brewless keeps only the reference. The keys stay the same ever after, so a page loaded before a deploy still connects after it. If the `realtime` container is not there yet, the step makes it on the image the web container runs now (or on the Brewless image), and the next step gives it the image it should run. The build can then use the address browsers will connect to.
2. **Realtime server**, after the web and worker containers have the new release. The `realtime` container gets its image, its variables and its settings: port 8080, at most one replica, the `timeout` and `min` from the file. The client prints the address next to this step, such as `wss://ws.shop.example.eu`.
3. **Realtime address**, near the end. On a domain of your own, this step makes the DNS record and attaches the hostname to the container (see below). It never fails a deploy that went live. What it could not do yet is shown as a notice at the end of the deploy.

A [rollback](/deployments/rollbacks) takes the realtime container back too. On `image: app` it runs the image of the earlier release. If the earlier release did not have a realtime server yet, the container is left as it is.

## The address

**On a domain of your own**, the address is `ws.` plus the host of the address your application calls its own (`APP_URL`, or the first domain in `domains` when your variables have no `APP_URL`). With `APP_URL=https://shop.example.eu` that gives `ws.shop.example.eu`. With `https://www.shop.example.eu` it gives `ws.www.shop.example.eu`, so set `host` if you want a different name.

- **DNS at Bunny DNS or Scaleway DNS.** When the zone is at Bunny DNS in your bunny.net account or at Scaleway DNS in your Scaleway project, Brewless makes a `CNAME` from that name to the container's own address. Another record already on that name is never changed: the deploy says which one is in the way.
- **DNS kept elsewhere.** Brewless makes no record. The deploy ends with the record you need to make at your DNS provider, such as `CNAME ws.shop.example.eu -> <container>.scw.cloud`.
- **The certificate.** Brewless attaches the hostname to the container as a custom domain at Scaleway, and Scaleway gets the certificate for it. Until Scaleway reports the hostname as ready, your application and your pages use the container's own address, which ends in `.scw.cloud`. The next deploy after that builds the own address into your pages.

**Without a domain of your own**, the address is the container's own. This applies when the environment answers only on its test address or on a provider's address, and when `host: false` is set.

**A hostname of your own**, such as `host: realtime.example.eu`, is handled the same way as `ws.`: Brewless makes the record where it can and attaches the name. A hostname under the Brewless test domain is refused.

The environment's **Realtime** page in the console shows which image runs, its size, the address browsers connect to right now, and what the container costs.

## What your application gets

From the first deploy with `realtime`, the web, release and worker containers get:

| Variable | Value |
| --- | --- |
| `REVERB_APP_ID` | The app id from your Secret Manager |
| `REVERB_APP_KEY` | The key, which is public: browsers send it |
| `REVERB_APP_SECRET` | The secret, as a secret |
| `REVERB_HOST` | The address above, without `wss://` |
| `REVERB_PORT` | `443` |
| `REVERB_SCHEME` | `https` |
| `BROADCAST_CONNECTION` | `reverb` |

`REVERB_HOST`, `REVERB_PORT`, `REVERB_SCHEME` and `BROADCAST_CONNECTION` are only set when your own variables leave them empty. A value of your own wins. The three keys are always the ones from your Secret Manager, because the server accepts no others.

> [!WARNING]
> Laravel's `.env.example` has `BROADCAST_CONNECTION=log`. If your environment's variables still say that, Brewless leaves it alone and your events go to the log. Change it to `reverb`, or remove it, and deploy.

Your frontend needs the key and the address when it is built, so the build gets `VITE_REVERB_APP_KEY`, `VITE_REVERB_HOST`, `VITE_REVERB_PORT` (`443`) and `VITE_REVERB_SCHEME` (`https`). Vite reads them from the environment of the build. They are part of what marks an image: when the address or the keys change, for example once the own hostname is ready, the same commit is built again. An image built for one environment is never reused for another.

## Setting up Laravel

1. Install broadcasting with Reverb, commit what it changes, and push:

   ```bash
   php artisan install:broadcasting --reverb
   ```

   This adds `laravel/reverb` to `composer.json`, `laravel-echo` and `pusher-js` to `package.json`, and writes `config/broadcasting.php` and `routes/channels.php`, and sets up Echo: `resources/js/echo.js`, or a `configureEcho()` call in an application made from a Vue or React starter kit. Both read the `VITE_` variables above. The REVERB values it writes to your local `.env` are for your own machine only. Brewless gives each environment its own.

2. Check the `reverb` connection in `config/broadcasting.php`. It reads the variables above:

   ```php config/broadcasting.php
   'reverb' => [
       'driver' => 'reverb',
       'key' => env('REVERB_APP_KEY'),
       'secret' => env('REVERB_APP_SECRET'),
       'app_id' => env('REVERB_APP_ID'),
       'options' => [
           'host' => env('REVERB_HOST'),
           'port' => env('REVERB_PORT', 443),
           'scheme' => env('REVERB_SCHEME', 'https'),
           'useTLS' => env('REVERB_SCHEME', 'https') === 'https',
       ],
   ],
   ```

3. Connect with Echo in `resources/js/echo.js`. It reads the `VITE_` variables of the build:

   ```js resources/js/echo.js
   import Echo from 'laravel-echo';
   import Pusher from 'pusher-js';

   window.Pusher = Pusher;

   window.Echo = new Echo({
       broadcaster: 'reverb',
       key: import.meta.env.VITE_REVERB_APP_KEY,
       wsHost: import.meta.env.VITE_REVERB_HOST,
       wsPort: import.meta.env.VITE_REVERB_PORT ?? 80,
       wssPort: import.meta.env.VITE_REVERB_PORT ?? 443,
       forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https',
       enabledTransports: ['ws', 'wss'],
   });
   ```

4. Authorise private and presence channels in `routes/channels.php`:

   ```php routes/channels.php
   use App\Models\Order;
   use App\Models\User;
   use Illuminate\Support\Facades\Broadcast;

   Broadcast::channel('orders', fn (User $user): bool => $user->can('viewAny', Order::class));
   ```

   The browser asks `/broadcasting/auth` on your web container for these, with the visitor's session. Public channels need no route.

5. Add `realtime` to `brewless.yml` and deploy.

> [!WARNING]
> With `image: app`, the realtime container starts your image with `reverb:start` as its argument. If your repository has its own `docker/entrypoint.sh`, it has to pass any argument that is not one of its own roles on to `php artisan`, as the [default entrypoint](/deployments/server-configuration#dockerentrypointsh) does. Otherwise the container starts the web server instead of Reverb.

## The cache of the realtime container

Reverb checks the cache every five seconds for a restart signal. With the cache in a database, that would be a query every five seconds all day, and a sleeping Serverless SQL Database would never sleep. On `image: app`, the realtime container therefore gets `CACHE_STORE=array` unless your application's cache store is `redis`. The web and worker containers keep the store you set. The restart signal is not needed: every deploy replaces the container. The Brewless image never touches your cache or your database. See [Cache](/running/cache) for the stores of the other containers.

## A Content-Security-Policy

If your application sends a `Content-Security-Policy`, allow the realtime address under `connect-src`:

```text
connect-src 'self' wss://ws.shop.example.eu
```

Until the own hostname is ready, your pages connect to the container's own address, which ends in `.scw.cloud`. Allow that too for as long as it is in use.

## Limits

Realtime is a container that holds connections open, on a platform built for requests. Within these limits it works well:

- **One container, a few hundred connections.** The realtime container has at most one replica, so one Reverb server holds every channel. Measured on Scaleway (5 November 2026): one container took 400 open connections without turning any away, and memory is what runs out first, about a third of a megabyte per connection. 256 MB holds about 300 connections, 512 MB comfortably more. Give it more `memory` before you get close.
- **At most an hour per connection.** Scaleway closes a connection when the container's `timeout` runs out, an hour at most. pusher-js, which Echo uses, connects again by itself.
- **A deploy reconnects everyone, about 30 seconds late.** A deploy replaces the container. Scaleway keeps the old one running for about 30 seconds after the new one is ready: browsers connected to the old one miss what is sent in that window, then the old one is cut (close code 1006) and pusher-js connects to the new one within a fraction of a second. So load the state again after a reconnect.
- **Missed events are not sent again.** Reverb sends an event to whoever is connected at that moment. A browser that was reconnecting misses it, so load the current state again after every reconnect (see below).
- **An open tab keeps it awake.** While a browser holds a connection, the container runs and is billed, even when nothing is being sent.
- **Every broadcast is a request to it.** Your application sends events to the realtime container over HTTPS, so a broadcast wakes a sleeping container even when nobody is listening. With `ShouldBroadcastNow` the request that broadcasts waits for that.
- **A start after a quiet spell.** With `min: 0`, the first browser after a quiet spell waits while the container starts: about 6 seconds for the Brewless image, longer for your whole application. Echo keeps trying until it is up. Set `min: 1` when that wait matters more than the cost.
- **Bots wake it too.** Once `ws.<domain>` has its certificate it is listed in public certificate logs, and scanners call it now and then. Each call wakes a sleeping container for about a quarter of an hour.

### Loading the state again after a reconnect

Mark every reconnect, but not the first connect:

```js resources/js/echo.js
let connectedBefore = false;

window.Echo.connector.pusher.connection.bind('connected', () => {
    if (connectedBefore) {
        window.dispatchEvent(new Event('echo:reconnected'));
    }

    connectedBefore = true;
});
```

A page then fetches again what it shows:

```js resources/js/orders.js
window.Echo.private('orders').listen('OrderShipped', (event) => {
    showOrder(event.order);
});

window.addEventListener('echo:reconnected', async () => {
    const response = await fetch('/orders/latest', { headers: { Accept: 'application/json' } });

    showOrders(await response.json());
});
```

## What it costs

The realtime container is billed by Scaleway on your own bill, like your other containers: per second it runs, for its processor and its memory. With `min: 0` it costs nothing while it sleeps.

| Size | Always on (`min: 1`) | Per hour it runs (`min: 0`) |
| --- | --- | --- |
| 140 thousandths of a vCPU, 256 MB | about €4.99 a month | about €0.0068 |
| 280 thousandths of a vCPU, 512 MB | about €9.99 a month | about €0.0137 |
| 560 thousandths of a vCPU, 1024 MB | about €19.97 a month | about €0.0274 |

These are Scaleway's list prices for Serverless Containers in `fr-par`, excluding VAT, checked on 5 November 2026: €0.0000100 per vCPU-second and €0.0000020 per GB-second, over a month of 730 hours. Your own prices at Scaleway are what count. The environment's **Realtime** page in the console works the figure out for the size you set.

## Turning it off

Remove `realtime` from `brewless.yml`, or set it to `false`, and deploy. From that release on, your application no longer gets the REVERB variables that Brewless set. The `realtime` container and the secret with its keys stay where they are: Brewless removes nothing of yours on its own. With `min: 0` a container that nobody connects to costs nothing. With `min: 1` it keeps running and costs money until you remove it in your Scaleway console under Serverless, Containers.

## When something goes wrong

The deploy says what went wrong in these words:

- **"brewless.yml asks for realtime on the Brewless image, which is in closed beta and not turned on for this organisation."** Request the beta (see [Requesting the beta](#requesting-the-beta)), or write `image: app`. Nothing was built.
- **"brewless.yml asks for realtime, and its keys go to the application next to its variables."** The environment's variables are not kept through Brewless, so there is nowhere to put the keys. Push them once with `brewless env:push` and deploy again. See [Environment variables](/running/variables).
- **"… has no realtime server Brewless knows how to start."** `image: app` needs a framework whose realtime server Brewless can start. Today that is Laravel.
- **"The keys of the realtime server in the secret … cannot be read."** The secret `<application>-<environment>-realtime` in your Secret Manager no longer holds the keys in a form Brewless can read. Remove it only if no browser needs to stay connected through the deploy: the next deploy makes new keys and builds them into your pages. A browser that still has a page open with the old key stops connecting until it loads the page again.
- **"The realtime container did not start on the new image."** On `image: app` this is most often `laravel/reverb` missing from `composer.json`, so `php artisan reverb:start` is not a command, or a `docker/entrypoint.sh` that does not pass the argument on to `php artisan`. Read what it printed with `brewless logs production --source=realtime`. The web and worker containers already run the new release at this point, so the release is live, but the steps after this one did not run. Fix the cause and deploy again.

At the end of a deploy that went live, a notice may tell you about the address:

- **"The realtime address is kept where Brewless does not reach. Make this record at your DNS provider: …"** Make the record it names. Until Scaleway has the certificate, browsers connect to the container's own address.
- **"Scaleway is getting the certificate for …"** This is normal on the first deploy with a new hostname. Browsers use the container's own address until the certificate is there, and the next deploy builds the own address into your pages.
- **"The realtime address could not be made: …"** The reason follows, for example a record already on the name that Brewless does not change. The release is live and browsers use the container's own address. Fix the reason, and the next deploy tries again.

On the Brewless image, a browser on a page whose host is not in `REVERB_ALLOWED_ORIGINS` has its connection closed with Pusher code 4009, and it does not try again. Add the domain under `domains` in `brewless.yml`, or set `APP_URL` to it, and deploy. On `image: app`, the allowed origins are the ones in your `config/reverb.php`.
