Running your application
Realtime
A WebSocket server for Laravel broadcasting next to your application, in your own Scaleway project. Browsers connect to it directly.
On this page
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:
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:
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. |
realtime: false, or no realtime key at all, means no realtime server. Every key and its bounds are listed in the brewless.yml reference.
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.ymlwithimage: brewlessis 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: brewlessgoes 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: brewlessis refused. Switch toimage: app, or removerealtime, 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.
- 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>-realtimein 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 therealtimecontainer 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. - Realtime server, after the web and worker containers have the new release. The
realtimecontainer gets its image, its variables and its settings: port 8080, at most one replica, thetimeoutandminfrom the file. The client prints the address next to this step, such aswss://ws.shop.example.eu. - 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 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
CNAMEfrom 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.examplehasBROADCAST_CONNECTION=log. If your environment's variables still say that, Brewless leaves it alone and your events go to the log. Change it toreverb, 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
-
Install broadcasting with Reverb, commit what it changes, and push:
php artisan install:broadcasting --reverbThis adds
laravel/reverbtocomposer.json,laravel-echoandpusher-jstopackage.json, and writesconfig/broadcasting.phpandroutes/channels.php, and sets up Echo:resources/js/echo.js, or aconfigureEcho()call in an application made from a Vue or React starter kit. Both read theVITE_variables above. The REVERB values it writes to your local.envare for your own machine only. Brewless gives each environment its own. -
Check the
reverbconnection inconfig/broadcasting.php. It reads the variables above: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', ], ], -
Connect with Echo in
resources/js/echo.js. It reads theVITE_variables of the build: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'], }); -
Authorise private and presence channels in
routes/channels.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/authon your web container for these, with the visitor's session. Public channels need no route. -
Add
realtimetobrewless.ymland deploy.
Warning
With
image: app, the realtime container starts your image withreverb:startas its argument. If your repository has its owndocker/entrypoint.sh, it has to pass any argument that is not one of its own roles on tophp artisan, as the default entrypoint 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 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:
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
memorybefore you get close. - At most an hour per connection. Scaleway closes a connection when the container's
timeoutruns 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
ShouldBroadcastNowthe 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. Setmin: 1when 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:
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:
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), 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:pushand deploy again. See Environment variables. - "… has no realtime server Brewless knows how to start."
image: appneeds 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>-realtimein 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: appthis is most oftenlaravel/reverbmissing fromcomposer.json, sophp artisan reverb:startis not a command, or adocker/entrypoint.shthat does not pass the argument on tophp artisan. Read what it printed withbrewless 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.