WebSockets with Laravel Broadcasting
You can use WebSockets in Laravel on AWS Lambda. Laravel Broadcasting and Laravel Echo work just like in the Laravel documentation .
You don’t need a Reverb server. With the bref/laravel-websockets package, API Gateway holds the WebSocket connections and Lambda runs your application when an event happens, so there is no server to manage.
Laravel 12 or later is required.
Installation
composer require bref/laravel-websockets
npm install laravel-echo pusher-jsEnable broadcasting
Do not run php artisan install:broadcasting: it requires choosing one of Laravel’s drivers. Instead, declare the channels file in bootstrap/app.php:
->withRouting(
web: __DIR__.'/../routes/web.php',
channels: __DIR__.'/../routes/channels.php',
// ...
)Then create routes/channels.php as described in the Laravel documentation .
Create the WebSocket API
Add this to serverless.yml:
provider:
# ...
environment:
# ...
BROADCAST_CONNECTION: api-gateway
BREF_WEBSOCKET_API_ID: !Ref WebsocketsApi
BREF_WEBSOCKET_STAGE: ${sls:stage}
iam:
role:
statements:
- Effect: Allow
Action: execute-api:ManageConnections
Resource: !Sub 'arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${WebsocketsApi}/*'
# The websocket function invokes itself when a connection opens
- Effect: Allow
Action: lambda:InvokeFunction
Resource: !Sub 'arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${self:service}-${sls:stage}-websocket'
functions:
# ...
websocket:
handler: Bref\LaravelWebsockets\Handler
runtime: php-84
timeout: 28
events:
- websocket: $connect
- websocket: $disconnect
- websocket: $defaultStore subscriptions in the database or DynamoDB
The application stores which connection listens to which channel, in your database by default:
| Database (default) | DynamoDB | |
|---|---|---|
| Use it for | Applications with a database and little traffic, back offices, preview environments | Many connections, frequent reconnections, or no database |
| Setup | Run php artisan migrate | Add the Lift plugin and a table to serverless.yml |
| Cost | Nothing more | Less than $3 per month for 1000 connections |
| Time per operation | 3 to 4 ms | 3 to 4 ms |
| First operation of a request | 12 to 29 ms | 34 to 47 ms |
| Reconnection bursts | Load your database, with one SQL connection per concurrent Lambda | No impact on your database |
If your database pauses when idle (like Aurora Serverless), note that each browser tab left open writes to it every 2 hours, which keeps it awake.
To use DynamoDB, install the Serverless Lift plugin:
npm install --save-dev serverless-liftThen enable it and create the table in serverless.yml:
provider:
# ...
environment:
# ...
BREF_WEBSOCKET_STORE: dynamodb
BREF_WEBSOCKET_DYNAMODB_TABLE: ${construct:websocket-subscriptions.tableName}
plugins:
# ...
- serverless-lift
constructs:
websocket-subscriptions:
type: database/dynamodb-single-table
gsiCount: 1The table is kept when you remove the environment: delete it in the AWS console if you no longer need it.
Connect Laravel Echo
Each environment has its own WebSocket API, so give its address to the frontend at runtime, in your layout:
@if (config('bref-websockets.api_id'))
<meta name="websocket-host" content="{{ config('bref-websockets.api_id') }}.execute-api.{{ config('bref-websockets.region') }}.amazonaws.com">
<meta name="websocket-stage" content="{{ config('bref-websockets.stage') }}">
@endifThen configure Echo:
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
const host = document.querySelector('meta[name="websocket-host"]')?.content;
const stage = document.querySelector('meta[name="websocket-stage"]')?.content;
if (host) {
window.Pusher = Pusher;
window.Echo = new Echo({
broadcaster: 'reverb',
key: 'bref', // Any value
wsHost: host,
wsPath: `/${stage}?`, // The trailing "?" is required
wsPort: 80,
wssPort: 443,
forceTLS: true,
enabledTransports: ['ws', 'wss'],
// Ping every 9 minutes instead of 2: each ping costs a Lambda invocation and 2 messages
activityTimeout: 540000,
});
}Load this file from your frontend entry point:
import './echo';Deploy
Bref Cloud
bref deployWith the database store, also run the migration: see Laravel Artisan commands.
Restrict origins
By default, any website can open a connection. To allow only your domains:
provider:
# ...
environment:
# ...
BREF_WEBSOCKET_ALLOWED_ORIGINS: 'example.com,*.example.com'Local development
The api-gateway driver needs AWS. Set BROADCAST_CONNECTION=log in your local .env: events are written to the logs, and Echo does not connect.
Inertia live props
Inertia live props work on API Gateway. Install the Echo package of your frontend framework, here for Vue:
npm install @laravel/echo-vueThen give Inertia the Echo instance created above, instead of calling configureEcho():
import './echo';
import { createInertiaApp } from '@inertiajs/vue3';
import { echo } from '@inertiajs/vue3/echo';
createInertiaApp({
live: echo({ resolve: () => window.Echo }),
// ...
});By default, each page listening to an event reloads the prop with an HTTP request, which means one more Lambda invocation per connection. When the value can be sent to all subscribers, put it in the event with Inertia::broadcastProps() to avoid those requests.
API Gateway or a Reverb server
API Gateway charges for each message delivered: about $1.25 to $1.50 per million all included in eu-west-3, and almost nothing when idle. What counts is connections × events, more than the number of connections.
| Use case | Messages per month | API Gateway | Use |
|---|---|---|---|
| Notifications: 20 connections, 1 event per minute | 0.9 million | $1 to $2 | API Gateway |
| Live back office: 20 connections, 1 event per second, office hours | 12.7 million | $19 | API Gateway |
| Dashboard on a screen: 20 connections, 1 event per second, 24/7 | 52 million | $78 | Either, depending on your budget |
| Public website: 1000 connections, 10 events per minute | 432 million | $545 | A Reverb server |
Start with API Gateway. Above about 50 million messages per month, or when an event regularly reaches many people, run Laravel Reverb on a small always-on server instead. An Amazon Lightsail container (nano, $7 per month with 500 GB of transfer included) handled 1000 connections and 10 events per minute at about 1% CPU.
Your events, channels and Echo code stay the same:
- install the
uvPHP extension in the Reverb container: without it, Reverb stops at about 1000 connections, - set
BROADCAST_CONNECTION=reverband theREVERB_*variables in the application, - in Echo, remove
wsPathand setwsHostto the address of the server, - use queued broadcasts (
ShouldBroadcast), because events cannot be sent while the server restarts.
A single server is a single point of failure: clients reconnect by themselves, but events sent during an outage are lost.
With Bref Cloud, the Metrics page of an environment shows its WebSocket messages and their estimated cost, as well as new connections, errors and latency.
Limits
- Events over 128 KB throw a
BroadcastException, because API Gateway refuses them. The size is measured once encoded, where quotes count twice, so a JSON payload can reach the limit at about 100 KB. - Messages are billed per 32 KB: a 33 KB event counts as two messages per connection.
- Encrypted private channels are not supported.
- API Gateway closes connections after 2 hours. Echo reconnects and subscribes again by itself, but events sent during those few hundred milliseconds are lost.
- API Gateway accepts 500 new connections per second by default, per account and region.
- If Lambda throttles the
websocketfunction, a subscription can be lost, and Echo does not retry it. Make sure your concurrency limit leaves room for reconnections. - Events sent in parallel have no ordering guarantee.