Realtime for Laravel: Reverb in your own Scaleway account

How Brewless runs Laravel Reverb on a serverless container in your own Scaleway account, the one-hour limit, and how to reconnect and refetch with Echo.

In this post
  1. Can Laravel Reverb run on a serverless container?
  2. What Brewless sets up
  3. The cache that would keep your database awake
  4. One hour, then reconnect
  5. A Content-Security-Policy
  6. What it costs, and when it sleeps
  7. The limits

Realtime is new and in beta: it works and is measured on Scaleway, but its limits and defaults may still change.

Laravel broadcasting needs a WebSocket server, and a WebSocket server is the one thing serverless hosting is least built for: it holds connections open, while a serverless container is made to answer a request and go back to sleep.

Brewless now has Realtime: Laravel Reverb in a container of its own, in your own Scaleway account. This post explains how that works, where the limits are and why they are there, and the two things your frontend should do to live with them.

Can Laravel Reverb run on a serverless container?

Yes, within limits. Scaleway Serverless Containers accept WebSocket connections, and each open connection counts as a request in flight until it closes. So a connection lives at most as long as the container's timeout, which is an hour at most, and one container holds as many connections as its concurrency allows, at most 80.

Those two numbers shape everything below. Within them, Reverb runs as it does anywhere else.

What Brewless sets up

You switch it on per environment:

brewless.yml
environments:
  production:
    realtime: true

On the next deploy Brewless does four things:

  1. It makes a Serverless Container named realtime in the environment's namespace, next to web and worker, with a timeout of an hour and at most one instance.
  2. It makes the Reverb app id, key and secret once, in a secret in your own Secret Manager. Brewless keeps the reference, never the values.
  3. It gives your web, release and worker containers REVERB_APP_ID, REVERB_APP_KEY, REVERB_APP_SECRET, REVERB_HOST, REVERB_PORT=443 and REVERB_SCHEME=https, and BROADCAST_CONNECTION=reverb when your app sets no connection of its own.
  4. It makes a DNS record for ws. in front of your app's own host and attaches that hostname to the container, which gets its own certificate. Until that address is ready, the container's own scw.cloud address is used.

All settings, with their defaults:

brewless.yml
environments:
  staging:
    realtime:
      image: app        # or brewless (closed beta)
      memory: 512       # MB; 512 on your app's image, 256 on the Brewless image
      min: 0            # 0 sleeps while nobody is connected, 1 is always on
      timeout: 3600     # seconds a connection may stay open, 10 to 3600
      host: true        # ws.<your host>, a hostname of your own, or false

Straight to the container

Browsers connect over wss:// to the Reverb container itself. Brewless is never a hop for your visitors, and Realtime does not change that.

There is no bunny.net pull zone in front of it either. A CDN earns its place by keeping responses at the edge, and a WebSocket has nothing to keep. A pull zone would only add a charge per connection minute and an idle timeout of its own.

What goes through the WebSocket stays between your app, your container and the browser. No hosted WebSocket service of another company is in that chain.

Two images to start from

With image: app, the default, the Reverb container runs your app's own release image, started as php artisan reverb:start. It gets all of your app's variables plus the Reverb ones, and follows every deploy and every rollback. It needs laravel/reverb in your composer.json.

Warning

If your app has its own docker/entrypoint.sh, it has to hand other arguments on to php artisan, as the Brewless base entrypoint does. Otherwise the container starts the web server instead of Reverb.

With image: brewless, the container runs a small image Brewless maintains: a bare Laravel skeleton with laravel/reverb. It gets only the Reverb keys and the origins that may connect, and starts in 256 MB. This one is in closed beta; you request it in the console, on the Realtime page of an environment.

The cache that would keep your database awake

php artisan reverb:restart works by putting a timestamp in the cache. A running Reverb server looks for it every five seconds, and restarts when it finds a newer one.

That is fine with Redis. With the cache in the database it is a query every five seconds, all day. A Scaleway Serverless SQL Database falls asleep five minutes after its last query, so a Reverb container with the database cache would keep it awake, and billed, all month.

So unless your app's cache store is Redis, Brewless starts the Reverb container on your app's image with CACHE_STORE=array (a cache in Object Storage would be asked just as often). The restart signal then never reaches it, and does not need to: every deploy replaces the container anyway. The Brewless image does not touch your database at all.

One hour, then reconnect

A connection is closed when the container's timeout is up, an hour at most, and when a deploy replaces the container. Neither is a failure. pusher-js, which Laravel Echo uses for Reverb, connects again by itself, usually within a second.

What does not come back is what happened in between. Reverb sends an event to whoever is connected at that moment and keeps nothing for later. A browser that was reconnecting missed it.

The fix is the same as for a laptop that wakes from sleep: when the connection comes back, ask your app for the current state.

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'],
});

// After every reconnect, not the first connect: load what may have been missed.
let connectedBefore = false;

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

    connectedBefore = true;
});

A page then listens for that event and fetches what it shows:

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());
});

The first half of that file is what php artisan install:broadcasting writes. Brewless gives the build VITE_REVERB_APP_KEY, VITE_REVERB_HOST, VITE_REVERB_PORT and VITE_REVERB_SCHEME, so it works without changes. Because those values differ per environment, they are part of what marks a built image: an image built with the keys of staging is never reused for production.

Tip

Private and presence channels are authorised by your own app at /broadcasting/auth, an ordinary request to your web container. Nothing about that changes on Brewless.

A Content-Security-Policy

If your app sends a Content-Security-Policy, the browser refuses a WebSocket to an address that connect-src does not name. Add the Reverb address:

Content-Security-Policy: connect-src 'self' wss://ws.example.com

Check it in a browser, not with curl: curl ignores the header, so a page can look fine there and still have no live updates for every visitor.

What it costs, and when it sleeps

The Reverb container is billed by Scaleway like your other containers, per second for processor and memory, on your own Scaleway bill. At Scaleway's list prices in fr-par before VAT (November 2026), €0.0000100 per vCPU-second and €0.0000020 per GB-second:

Size Always on Sleeping when idle
140 mvCPU, 256 MB about €5.00 a month only while connected
280 mvCPU, 512 MB about €10.00 a month only while connected

Always on is min: 1; sleeping when idle is min: 0.

With min: 0, the default, the container scales to zero after the last browser disconnects. The first visitor after a quiet spell waits for it to start; pusher-js keeps trying until it is up.

One thing to keep in mind: an open tab is an open connection. While somebody leaves your dashboard open, the container runs and is billed, also when nothing is being sent.

The limits

Limit Why
One Reverb container per environment Spreading connections over several needs Reverb's Redis scaling, which Realtime does not use
A few hundred open connections Measured: one container held 400; memory runs out first, about a third of a megabyte per connection
At most an hour per connection The container's timeout; Echo reconnects
A deploy reconnects everyone The container is replaced; for about 30 seconds the old one keeps its browsers and they miss what is sent
No replay of missed events Reverb sends to whoever is connected; refetch after reconnecting

For live notifications, a dashboard that updates, or a status page for a team, that is plenty. For thousands of open connections per app, it is not, and it is better to know that now.

The settings are in the brewless.yml reference, and the overview is on the Realtime page. Why other things keep a Laravel container awake is in Why your Laravel container never scales to zero.

Frequently asked questions

Can I run Laravel Reverb on serverless containers?

Yes, within limits. Scaleway Serverless Containers accept WebSocket connections, and a connection stays open until the container's timeout, at most an hour. Brewless runs Reverb on one such container per environment, which held 400 open connections when we measured it.

Does WebSocket traffic go through Brewless or bunny.net?

No. Browsers connect over wss:// straight to the Reverb container in your own Scaleway project. Brewless is never a hop for visitor traffic, and there is no CDN in front of the container because WebSockets have nothing to cache.

What happens to open connections when I deploy?

A deploy replaces the Reverb container, so every open connection is closed and Laravel Echo connects again by itself, usually within a second. Load the current state of the page after that reconnect, because events sent in between are not sent again.

Why does the Reverb container not use my database cache?

Reverb looks in the cache for its restart signal every five seconds. With the cache in the database that is a query every five seconds, which would keep a Serverless SQL Database awake all month. Brewless therefore gives the Reverb container an array cache when your app's cache store is the database.

What does Realtime cost at Scaleway?

The Reverb container is billed like your other containers, per second for processor and memory. Always on, 140 mvCPU and 256 MB come to about €5 a month at Scaleway's list prices in fr-par before VAT. With min 0 it costs nothing while nobody is connected.

All posts Read this post as markdown

Try it on your own app

Brewless deploys your Laravel app to Scaleway and bunny.net, in your own accounts. Start free with one application, production included.