Pusher(プッシャー)は、WebSocketサーバーを自前で運用せずにリアルタイム通信を追加できるマネージドサービスです。LaravelのBroadcasting機能が公式にPusherドライバーを備えているため、チャットや通知の即時更新は数十行のコードで動きます。ただしLaravel 11以降は設定ファイルの扱いと環境変数名が変わっており、2022〜2023年ごろの記事どおりに書くと接続できません。この記事では、Laravel 13.38.0の新規プロジェクトで確認した導入手順を起点に、イベントの送受信、入力中表示などのクライアントイベント、本番で詰まるトランザクションとキューの順序、無料枠の範囲、自前ホストのLaravel Reverbとの選び方までを、実装する順番で整理します。
まとめ:Laravel 13でPusherを使うときの要点
- 導入は
php artisan install:broadcasting --pusher。Laravel 11以降の新規プロジェクトにはconfig/broadcasting.phpもroutes/channels.phpも無く、このコマンドが生成する。 - 環境変数は
BROADCAST_CONNECTION=pusher。旧称のBROADCAST_DRIVERは読まれない。フロント側のキーはVITE_PUSHER_APP_KEYで、Laravel Mix時代のMIX_PUSHER_APP_KEYは値が入らない。 - ブロードキャストはキュー経由。
queue:workが止まっていると届かない。DBトランザクション内で発火するならShouldDispatchAfterCommitを付ける。 - 入力中表示(whisper)はPusherのダッシュボードで「Client Events」を有効にしないと送れない。送れるのはprivateとpresenceチャンネルだけ。
- 無料のSandboxは100同時接続・1日20万メッセージ。超え続けるならStartup(月49ドル)に上げるか、自前運用のReverbへ移る。コードはほぼそのまま移せる。
Pusherとは:WebSocketをマネージドで提供するリアルタイム通信サービス
Pusherは、WebSocket接続の維持・スケール・フェイルオーバーを肩代わりするリアルタイム通信基盤です。サーバーがイベントを発行すると、Pusherが接続中のブラウザへ配信する「パブリッシュ/サブスクライブ」型で、アプリケーション側はAPIキーを設定するだけで済み、WebSocketサーバーの常駐プロセスもRedisも用意しません。この記事で扱うのはWebSocket製品のChannelsで、モバイルプッシュ通知のBeamsは別製品です。運営はPusher公式の採用ページにあるとおり2020年11月からMessageBirdの傘下で、セキュリティページではサービス提供者をMessageBird UK Ltd.と記載しています。
ポーリング(一定間隔でのHTTPリクエスト)と違い、サーバー側の変化がそのまま画面に届くため、チャット・通知バッジ・進捗バー・オンライン表示に向きます。WebSocketそのものの仕組み(ハンドシェイクやプロキシ越えの注意点)はWebSocketとは?仕組み・ハンドシェイクからNode.js実装・運用の判断まで実装者向けに解説で扱っています。Pusherはこの層を丸ごと外部に預ける選択肢です。
料金プラン:無料Sandboxの上限と有料プランへ上げる分岐点
2026年10月27日時点のPusher Channels公式の料金ページの公開プランは次のとおりです(課金はドル建て)。
| プラン | 月額 | 同時接続 | メッセージ/日 |
|---|---|---|---|
| Sandbox | 無料 | 100 | 20万 |
| Startup | 49ドル | 500 | 100万 |
| Pro | 99ドル | 2,000 | 400万 |
| Business | 299ドル | 5,000 | 1,000万 |
| Premium | 499ドル | 10,000 | 2,000万 |
| Growth | 699ドル | 15,000 | 4,000万 |
| Plus | 899ドル | 20,000 | 6,000万 |
| Growth Plus | 1,199ドル | 30,000 | 9,000万 |
| Enterprise | 個別見積もり | 個別 | 個別 |
判断の目安は同時接続の100です。数えるのは同時に確立している接続で、同じ利用者・ブラウザでも、複数タブでそれぞれPusherを初期化すると複数接続になります。社内ツールや管理画面でも同時接続数とメッセージ数の確認は必要ですが、一般ユーザー向けサービスで常時100人がページを開いているなら有料プランが前提になります。メッセージはpublish 1回+購読者数ぶんの配信=1+Nで数えるので、50人が購読するチャンネルへ1回送れば51メッセージです。
見積もりは「1日の送信回数×(1+平均購読者数)」です。20人のチャットルームが30室、1室で1日200件の投稿なら200×30×21=12万6,000メッセージです。ただし全室を別々の利用者が同時に使うと600接続となり、Sandboxの100接続を超えます。また、既読や入力中の表示も配信すると、その送信頻度と配信先の接続数に応じてメッセージ数が増えます。何をリアルタイムで流し、何を画面遷移時の再取得で済ませるかを先に決めておくと、プランを1段上げずに済みます。
Laravel 13でPusherを導入する手順:install:broadcastingから.envまで
install:broadcasting –pusherが行う処理
手順はLaravel 13.xのBroadcasting公式ドキュメントの「Pusher Channels」節にあります。コマンドは1行です。
php artisan install:broadcasting --pusher
Laravel 13.38.0の新規プロジェクトで実行すると、次の順に処理が進みます(コマンドの実装は Illuminate/Foundation/Console/BroadcastingInstallCommand.php)。
config/broadcasting.phpを公開し、routes/channels.phpを作る。bootstrap/app.phpのwithRouting()にchannels:を足し、.envをBROADCAST_CONNECTION=pusherに書き換える(新規プロジェクトの初期値はlog)。- Pusherの App ID・Key・Secret を尋ね、クラスタを選ばせる。選択肢は mt1・us2・us3・eu・ap1・ap2・ap3・ap4・sa1 の9つ。
- Composerで
pusher/pusher-php-serverを入れる。 - フロント側の設定を足す。素のJavaScript構成なら
resources/js/echo.jsを作ってbootstrap.js(無ければapp.js)から読み込み、React・Vueのスターターキットならapp.tsxなどへconfigureEcho()を差し込む。 npm install --save-dev laravel-echo pusher-jsとnpm run buildを実行する(pnpm・yarn・bunのロックファイルがあればそちらを使う)。
3番目は対話入力なので、-n(非対話)を付けると「Pusher App ID」の入力が空のまま検証で止まります。CIやDockerfileの中で実行するのではなく、開発者の手元で1回だけ実行してください。2026年10月時点の最新は laravel/framework v13.38.0、pusher/pusher-php-server 7.3.0、laravel-echo 2.6.1、pusher-js 8.6.0 です。PHP側SDKはpusher/pusher-http-phpのリポジトリで開発されています。
.envの設定:BROADCAST_CONNECTIONとVITE_プレフィックス
コマンドが .env に書く変数は次の形です。PUSHER_APP_CLUSTER はPusherダッシュボードでアプリを作ったときのクラスタと一致させます。東京なら ap3 で、Pusherのクラスタ一覧に載っています。手で設定する場合、公式ドキュメントの例は既定の mt1(米国東部)のままなので、そのまま写すと接続が確立しません。
BROADCAST_CONNECTION=pusher
PUSHER_APP_ID="your-app-id"
PUSHER_APP_KEY="your-app-key"
PUSHER_APP_SECRET="your-app-secret"
PUSHER_APP_CLUSTER="ap3"
PUSHER_PORT=443
PUSHER_SCHEME="https"
VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
VITE_PUSHER_HOST="${PUSHER_HOST}"
VITE_PUSHER_PORT="${PUSHER_PORT}"
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
PUSHER_HOST は空のままで構いません。config/broadcasting.php は空なら api-+クラスタ名+.pusher.com を送信先にします。App SecretはPHP側だけが使います。VITE_ を付けた変数はビルド成果物に埋め込まれてブラウザから読めるため、Secretに VITE_ を付けてはいけません。.env を書き換えたあとは、php artisan config:clear と npm run build をやり直さないと古い値で動き続けます。
MIX_PUSHER_APP_KEYが効かない原因とVITE_への移行
古い記事や既存プロジェクトには process.env.MIX_PUSHER_APP_KEY が残っています。Laravel 9系の途中でビルドツールがLaravel MixからViteへ変わったため、この書き方はViteでは値を解決できず undefined になり、Pusherへの接続が失敗します。JS側は import.meta.env.VITE_PUSHER_APP_KEY へ、.env 側は MIX_ 接頭辞を VITE_ へ置き換えてください。Viteの環境変数の扱いはLaravel Viteとは?使い方・@viteの書き方と本番ビルドの落とし穴【Laravel 13実測】で扱っています。
イベントを送信してLaravel Echoで受信するコード
ShouldBroadcastイベントの配信先と送信データ
以下は ShouldBroadcast を実装するチャットイベントの例です。Messageモデルのroom_id・body・userリレーションと、Userモデルのroomsリレーションが定義済みで、ログインできる構成を前提にしています。broadcastOn() で配信先チャンネルを、broadcastAs() でフロントが受け取るイベント名を、broadcastWith() で送るデータを決めます。
php artisan make:event MessageSent
<?php
// app/Events/MessageSent.php
namespace App\Events;
use App\Models\Message;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Contracts\Events\ShouldDispatchAfterCommit;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class MessageSent implements ShouldBroadcast, ShouldDispatchAfterCommit
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(public Message $message) {}
public function broadcastOn(): PrivateChannel
{
return new PrivateChannel('chat.' . $this->message->room_id);
}
public function broadcastAs(): string
{
return 'message.sent';
}
// 送るデータを絞る(未指定ならpublicプロパティ全体が送られる)
public function broadcastWith(): array
{
return [
'id' => $this->message->id,
'body' => $this->message->body,
'user' => $this->message->user->only(['id', 'name']),
];
}
}
コントローラーでは broadcast(new MessageSent($message)); と発火します。broadcastWith() を書かないとpublicプロパティがそのままJSONで届くため、モデルを丸ごと渡すとメールアドレスなど画面に不要な列まで送られます。ShouldDispatchAfterCommit を付けている理由は、後述の「本番で詰まる箇所」で説明します。
echo.jsの初期化とlistenによるイベント受信
install:broadcasting --pusher が作る resources/js/echo.js は次の内容です(Laravel 13.38.0の echo-js-pusher.stub)。古い記事にある encrypted: true は使わず、forceTLS: true になっています。
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
window.Pusher = Pusher;
window.Echo = new Echo({
broadcaster: "pusher",
key: import.meta.env.VITE_PUSHER_APP_KEY,
cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER,
forceTLS: true,
wsHost: import.meta.env.VITE_PUSHER_HOST,
wsPort: import.meta.env.VITE_PUSHER_PORT,
wssPort: import.meta.env.VITE_PUSHER_PORT,
enabledTransports: ["ws", "wss"],
});
購読は画面側で書きます。以下はroomIdを12とした例です。後述の匿名イベント送信例はchat.1宛てなので、組み合わせて試すときは送信側と受信側のIDを一致させてください。
const roomId = 12;
window.Echo.private(`chat.${roomId}`)
.listen('.message.sent', (e) => {
console.log(e.body, e.user.name);
});
// 画面を離れるときは購読を解除する
// window.Echo.leave(`chat.${roomId}`);
broadcastAs() で名前を付けた場合、listen() の引数は先頭にドットを付けた '.message.sent' です。ドットを落とすとEchoは先頭に App\Events\ を付けたクラス名として探すため、イベントを取りこぼします。SPAで画面遷移のたびに購読を重ねると同じイベントを何度も受け取るので、画面を離れるときに leave() を呼んでください。
React・Vue・SvelteのuseEchoフックによる購読
Laravel EchoにはReact・Vue・Svelte向けのフックがあり(@laravel/echo-react・@laravel/echo-vue・@laravel/echo-svelte、いずれも2.6.1)、スターターキットに install:broadcasting を実行すると configureEcho() の呼び出しが入ります。useEcho はコンポーネントが外れるときに自動でチャンネルを抜けるので、上の leave() を書き忘れる事故が起きません。
import { useEcho } from "@laravel/echo-react";
function ChatRoom({ roomId }) {
useEcho(`chat.${roomId}`, ".message.sent", (e) => {
console.log(e.body);
});
// ...
}
React側の構成(Inertia・SPA・Sanctum)の選び方はLaravelとReactの連携方法|3つの構成の選び方とInertia・Sanctumの設定【Laravel 13】にまとめています。
routes/channels.phpのプライベートチャンネル認可
PrivateChannel を使うと、クライアントは購読時に /broadcasting/auth へ認可リクエストを送ります。許可するルールは routes/channels.php に書きます。
// routes/channels.php
Broadcast::channel('chat.{roomId}', function ($user, $roomId) {
return $user->rooms()->whereKey($roomId)->exists();
});
trueを返さないと403になり購読が失敗します。ログイン状態でしか通らないため、APIを別ドメインに置くSPAのようにセッションCookieが送られない構成では、withCredentials やSanctumのステートフルドメインの設定が先に要ります。Cookieセッションとトークンのどちらで認証するかはLaravel Sanctumとは:SPAとモバイルのトークン認証とCookieセッションの使い分けが参考になります。
プレゼンスチャンネルによる参加ユーザー一覧の表示
「いまこのルームに誰がいるか」を出すときは、プライベートチャンネルの拡張であるプレゼンスチャンネルを使います。違いは認可コールバックの戻り値で、真偽値ではなく画面に出してよいユーザー情報の配列を返します。
// routes/channels.php
Broadcast::channel('room.{roomId}', function ($user, $roomId) {
if ($user->rooms()->whereKey($roomId)->exists()) {
return ['id' => $user->id, 'name' => $user->name];
}
});
window.Echo.join(`room.${roomId}`)
.here((users) => { renderMembers(users); })
.joining((user) => { addMember(user); })
.leaving((user) => { removeMember(user); })
.error((error) => { console.error(error); });
here は参加直後に現在のメンバー全員を、joining・leaving は増減のたびに1人ずつ受け取ります。返した配列はチャンネル内の全員に見えるため、メールアドレスや権限情報は入れないでください。
クライアントイベント(whisper)による「入力中」の表示
「〇〇さんが入力中」のように、Laravelのサーバーを通さずブラウザ同士で軽い通知を送るのがクライアントイベントです。Echoでは whisper で送り、listenForWhisper で受けます。
const channel = window.Echo.private(`chat.${roomId}`);
// 送る側(入力欄のinputイベントで呼ぶ)
channel.whisper('typing', { name: currentUser.name });
// 受ける側
channel.listenForWhisper('typing', (e) => {
showTyping(e.name);
});
Pusherでは条件が4つあります。Pusherのイベントのドキュメントによると、アプリの設定でClient Eventsを有効にすること、送れるのはprivateとpresenceチャンネルだけであること、1接続あたり毎秒10メッセージを超えると拒否されること、そして購読が成功してから送ることです。有効化を忘れても画面には何も出ないので、「whisperだけ届かない」ときはダッシュボードのApp Settingsを最初に見てください。キー入力ごとに送ると毎秒10件を簡単に超えるため、300ミリ秒程度の間引き(debounce)を入れるのが前提です。
toOthersとX-Socket-IDによる送信者への二重表示防止
チャットでは、投稿者の画面にはHTTPレスポンスで自分の投稿を表示し、同じ内容がWebSocket経由でもう一度届いて二重に並ぶ、という不具合がよく起こります。送信側で toOthers() を付ければ止まります。
broadcast(new MessageSent($message))->toOthers();
Laravelはリクエストの X-Socket-ID ヘッダーから送信者の接続を特定し、その接続を配信先から外します。グローバルなAxiosインスタンスならEchoがヘッダーを自動で付けますが、fetch や独自のHTTPクライアントでは付きません。その場合は window.Echo.socketId() の値を自分でヘッダーに載せます(公式ドキュメントの「Only to Others」節)。
本番で詰まる箇所:トランザクション・キュー停止・ペイロード上限
ここでは、DBトランザクションと配信の順序、キュー投入・送信時の例外処理、Pusherのペイロード上限を確認します。
ShouldDispatchAfterCommitによるコミット後のイベント発火
トランザクションの中でイベントを発火すると、コミット前にキューワーカーがジョブを処理し、まだ保存されていないレコードを読みにいくことがあります(13.xドキュメントの「Broadcasting and Database Transactions」節)。イベントクラスに ShouldDispatchAfterCommit を実装すると、開いているトランザクションがすべてコミットされてからキューに積まれます。Laravel 13.38.0(SQLite・databaseキュー)で、DB::transaction() の中で発火した直後に jobs テーブルの件数を数えた結果は次のとおりです。
| イベントクラス | トランザクション内の件数 | コミット後の件数 |
|---|---|---|
| ShouldDispatchAfterCommitあり | 0 | 1 |
| なし | 1 | 1 |
databaseキューはジョブの書き込み自体が同じトランザクションに入るので実害が出にくいのですが、Redisキューに切り替えた時点でワーカーがコミット前のジョブを拾えるようになります。ロールバックされた場合も、付けていればジョブは破棄されます。キュー接続ごとに after_commit を true にする方法もありますが、イベント単位で付けておけばキュー設定を変えても挙動が変わりません。キュー全般の設計はLaravel Queueの使い方|ジョブ定義・worker常駐・失敗ジョブ運用を13.38で実測にまとめています。
キューやPusherが落ちても画面を止めないShouldRescue
キューへの投入に失敗した場合や同期送信中に例外が発生した場合は、HTTPリクエストが失敗することがあります。非同期ワーカー内で起きたPusherへの送信失敗は、通常、投稿時のHTTPレスポンスとは別に処理されます。通知は本体の処理に付随する機能なので、ShouldRescue を実装しておくと、キュー投入時・同期送信時の例外を記録したうえで処理を続けます。
use Illuminate\Contracts\Broadcasting\ShouldRescue;
class MessageSent implements ShouldBroadcast, ShouldDispatchAfterCommit, ShouldRescue
ShouldRescueが捕捉したキュー投入時や同期送信時の例外について、自動再送は行われません。一方、非同期ワーカー内の送信例外はこのインターフェースでは捕捉されず、ジョブの試行回数などの設定に従って再試行されます。「メッセージは保存されたが相手の画面に出ない」状態を許容できる機能(通知バッジや進捗表示)に限って付け、決済の完了通知のように届かないと困るものは、画面遷移時の再取得を必ず併用してください。
イベントデータの10KB上限とHTTP 413エラー
Pusher ChannelsのHTTP APIリファレンスでは、1イベントのdataは10KBまでで、超えるとHTTP 413が返ります。チャットの本文やコメント一覧をそのまま載せると簡単に超えるので、broadcastWith() ではIDと短い要約だけを送り、本文は受け取った側がAPIで取りにいく形にしておくと上限を気にせずに済みます。
暗号化プライベートチャンネル:Pusher側にも中身を見せない設定
プライベートチャンネルは「誰が購読できるか」を制限するだけで、データ自体はPusherのサーバーを平文で通ります。医療や人事のように外部サービスへ内容を渡したくないデータを流すなら、エンドツーエンド暗号化のチャンネルを使います。設定は3か所です。
# 32バイトの鍵を作る
openssl rand -base64 32
出力された鍵を .env の PUSHER_ENCRYPTION_MASTER_KEY に保存します。この鍵はPHP側だけで管理し、VITE_ 変数には入れません。設定キャッシュを更新し、起動済みのキューワーカーも再起動してください。
// config/broadcasting.php の pusher.options に追加
'encryption_master_key_base64' => env('PUSHER_ENCRYPTION_MASTER_KEY'),
// イベント側
use Illuminate\Broadcasting\EncryptedPrivateChannel;
public function broadcastOn(): array
{
return [new EncryptedPrivateChannel('orders.' . $this->order->id)];
}
// フロント側:以下のimportでecho.jsの既存のPusher importを置換する
// window.Pusherの設定はnew Echo(...)より前に置く
// 既に生成済みのEchoへ後から追加しても復号対応には切り替わらない
import Pusher from 'pusher-js/with-encryption';
window.Pusher = Pusher;
window.Echo.encryptedPrivate(`orders.${orderId}`)
.listen('OrderShipmentStatusUpdated', (e) => { console.log(e.order); });
認可は通常のプライベートチャンネルと同じコールバックがそのまま使われます。注意点は移行時で、13.xのドキュメントが暗号化チャンネルの対象に挙げているのはPusher ChannelsとMercureだけです。暗号化チャンネルを使っている機能がある場合、後述のReverbへの移行はコードの書き換えだけでは済まないので、移行の判断材料に入れてください。
動作確認:tinker・匿名イベント・Debug consoleによる送信テスト
画面を作り込む前に、サーバーからPusherへイベントが出ているかを単体で確かめておくと、後の切り分けが楽になります。
- Pusherダッシュボードで対象アプリを開き、「Debug console」を表示したままにする。
- 別ターミナルで
php artisan queue:workを起動する(sendNow()やShouldBroadcastNowなら不要)。 php artisan tinkerからイベントを発火する。
イベントクラスを作る前なら、Broadcast ファサードの匿名イベントが手軽です。クラス無しでチャンネル名・イベント名・データを指定して送れます。
php artisan tinker
> Broadcast::private('chat.1')->as('message.sent')->with(['body' => 'hello'])->sendNow();
Debug consoleに private-chat.1 宛ての message.sent が出れば、認証情報・クラスタ・送信経路は正しく動いています(BROADCAST_CONNECTION=log にすると同じ内容が storage/logs/laravel.log に「Broadcasting [message.sent] on channels [private-chat.1]」と出るので、Pusherに繋ぐ前の確認にも使えます)。ここに何も出ない場合はPHP側の設定・キュー・送信経路を確認します。出ているのに受信できない場合は、Echoの設定に加え、チャンネル名、Laravel側の購読認可、ブラウザの接続状態も確認してください。
pusher-php-server単体でのイベント送信
Laravelの設定キャッシュやキューを疑う前に、認証情報そのものが正しいかを確かめたいときは、SDKを直接呼びます。このスクリプトはLaravelの.envを読み込まないため、実行前にPUSHER_APP_KEY・PUSHER_APP_SECRET・PUSHER_APP_ID・PUSHER_APP_CLUSTERをシェルの環境変数として設定してください。
<?php
// check_pusher.php(プロジェクト直下で PUSHER_APP_KEY=... php check_pusher.php)
require __DIR__ . '/vendor/autoload.php';
$pusher = new Pusher\Pusher(
getenv('PUSHER_APP_KEY'),
getenv('PUSHER_APP_SECRET'),
getenv('PUSHER_APP_ID'),
['cluster' => getenv('PUSHER_APP_CLUSTER') ?: 'ap3', 'useTLS' => true]
);
$pusher->trigger('test-channel', 'test-event', ['message' => 'hello']);
echo "sent\n";
Debug consoleの test-channel に test-event が出れば、キー・シークレット・クラスタの組み合わせは正しいと判断できます。例外が出るなら、Laravelのコードではなく認証情報かネットワーク(社内プロキシでの443番の遮断など)を調べてください。
キューワーカーの常駐:Supervisorによるqueue:workの監視と再起動
本番で queue:work が落ちると、その瞬間からリアルタイム通知が黙って止まります。エラー画面も出ないため気づくのが遅れがちです。Laravel 13.xのQueuesドキュメントはSupervisorでの常駐を案内しています。
; /etc/supervisor/conf.d/laravel-worker.conf
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/app/artisan queue:work --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/var/www/app/storage/logs/worker_%(process_num)02d.log
stopwaitsecs=3600
デプロイのたびに php artisan queue:restart を実行しないと、ワーカーは古いコードのまま動きます。通知とCSV取り込みのような重いジョブを同じキューに入れると、通知がバッチの後ろに並んで数分遅れるので、イベントに broadcastQueue() を定義して専用キューへ分け、そのキューだけを処理するワーカーを別に立てるのが扱いやすい構成です。
PusherかReverbか:外部サービスと自前運用の判断基準
2024年のLaravel 11で、ファーストパーティのWebSocketサーバーLaravel Reverbが加わりました(2026年10月時点の最新は v1.12.0)。ReverbはPusherプロトコル互換なので、Echo側は broadcaster を 'reverb' に変更し、接続先ホスト・ポート・アプリキーなどもReverb用に設定します。通常のイベントや認可のコードは流用できますが、暗号化プライベートチャンネルは別の設計が必要です。13.xのドキュメントが挙げるドライバーはReverb・Pusher Channels・Ably・Mercureの4つで、install:broadcasting にもそれぞれのオプションがあります。
結論は、常時接続が数百までで、サーバー運用の手を増やしたくないならPusher、接続数が数千規模に伸びる見込みがあり、常駐プロセスの監視と再起動を自分たちで回せるならReverbです。Pusherの費用は接続数に比例して階段状に上がり、5,000接続で月299ドルです。Reverbはサーバー代だけで済む代わりに、プロセス監視・スケールアウト・TLS終端・再接続が殺到したときの負荷が運用側の責任になります。費用比較には、Pusherの月額だけでなく、Reverbの監視構築・保守・障害対応にかかる工数も含めてください。監視の作り込みが必要になったらLaravelでPrometheus・Grafana監視を実装する手順|PHPのメトリクス計測とアラート設定が参考になります。
逆に採用すべきでないのは、接続数のピークやメッセージ数を見積もらず、Sandboxの無料枠だけで一般公開の本番サービスを賄う使い方です。告知直後のアクセス集中で100接続にあっさり届き、上限を超えた接続はPusherのプロトコル仕様のエラーコード 4004(接続数の割当超過)で拒否されます。4000〜4099は「同じ条件で再接続しても成功しない」区分なので、クライアントの自動再接続では回復しません。
Pusher・Reverb・laravel-echo-serverの費用と運用負荷
| 観点 | Pusher | Laravel Reverb | laravel-echo-server |
|---|---|---|---|
| 提供形態 | マネージドSaaS | 自前ホスト(PHP) | 自前ホスト(Node.js) |
| 費用 | 無料〜月額ドル課金 | サーバー代のみ | サーバー代のみ |
| 常駐プロセス | 不要 | reverb:startを常駐 | Node.jsを常駐 |
| 水平スケール | Pusher側が担う | Redisで複数台に分散 | 自前で構成 |
| 暗号化チャンネル | 対応 | ドキュメントの対象外 | – |
| 保守状況 | 商用サービス | Laravel公式 | 2023年11月にアーカイブ |
コードの互換性が高いので、最初はPusherで立ち上げ、接続数と料金が見えた段階でReverbへ移す順序も現実的です。
Reverbを本番で動かすときに引き受ける作業
Laravel 13.xのReverbドキュメントの本番運用の節によると、WebSocketは1接続ごとにファイルディスクリプタを1つ使うため、OSの上限(ulimit -n)とSupervisorの minfds を接続数に合わせて引き上げます。1台で足りなくなったら REVERB_SCALING_ENABLED=true と中央のRedisを用意し、複数のReverbサーバーをロードバランサーの背後に並べます。デプロイ後は php artisan reverb:restart を呼ばないと新しいコードが反映されません。これらを回せる体制がない段階でReverbを選ぶと、障害時の切り分け先が増えるだけになります。
PusherからReverbへ切り替える手順と.envの書き換え箇所
php artisan install:broadcasting --reverb を実行すると、Reverbのパッケージと REVERB_ 系の変数が用意されます。そのうえで .env と echo.js を切り替えます。
BROADCAST_CONNECTION=reverb
REVERB_APP_ID=my-app-id
REVERB_APP_KEY=my-app-key
REVERB_APP_SECRET=my-app-secret
REVERB_HOST="ws.example.com"
REVERB_PORT=443
REVERB_SCHEME=https
VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
VITE_REVERB_HOST="${REVERB_HOST}"
VITE_REVERB_PORT="${REVERB_PORT}"
VITE_REVERB_SCHEME="${REVERB_SCHEME}"
// resources/js/echo.js(Reverb版)
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'],
});
イベントクラス・routes/channels.php・listen() は触りません。切り替え当日もPusher側のアプリを残しておけば、BROADCAST_CONNECTION と echo.js を戻し、npm run build を実行します。PHP側の設定キャッシュも更新し、キューワーカーなどの常駐プロセスを再起動して、Pusherへの送信と受信を確認します。TLS終端をNginxで行う場合は、WebSocketのUpgradeヘッダーを転送する設定を忘れないでください。
laravel-echo-server:新規採用を外すべき理由
かつての自前ホストの定番だったSocket.io製の laravel-echo-server は、2023年11月15日にGitHubリポジトリがアーカイブされ、読み取り専用になりました。新規採用は避け、動いている既存プロジェクトもReverbへの移行計画を立ててください。Socket.IOとWebSocketの違いはSocket.IOとは?WebSocketとの違いから実装・スケールまで、古いスターターキットの置き換えはLaravel Breezeはなぜ非推奨に?Laravel 13で使う手順と後継スターターキット4種・移行判断で扱っています。
WebSocketが要らない場面:SSEやポーリングで足りる要件
管理画面に「処理が終わりました」と1回出す、ダッシュボードの数値を1分ごとに更新する、といったサーバーからの一方向で低頻度の通知だけなら、Server-Sent Events(SSE)や数十秒間隔のポーリングで足りることが多く、外部サービスの契約も常駐プロセスも増えません。比較はSSEとWebSocketの違いと使い分け|WebRTC・WebTransportまで比較【2026年9月】にまとめています。チャット機能そのものを外部SDKに任せる選択肢はSendbirdとは?チャットSDKの料金と実装手順、自前WebSocket実装との使い分けで比べています。
リアルタイム通知を含む業務システムを新しく作る場合、PusherかReverbかの選択は、認証方式・キュー構成・監視体制と切り離して決められません。要件整理から実装・運用設計までをまとめて相談したいときは、一創のフルスクラッチ開発でLaravelを含むPHPの受託開発を承っています。
Pusherに接続できないときの切り分け:症状別の原因
- ブラウザのコンソールに何も出ない:Echoが初期化されていない。
bootstrap.jsかapp.jsでecho.jsを読み込んでいるか、npm run build後のバンドルが配信されているかを確認する。 - キーがundefined/接続直後に切断:
MIX_接頭辞のまま、またはVITE_変数を追加した後にビルドし直していない。 - WebSocketは張れるがイベントが来ない:クラスタの不一致、
listen()のドット漏れ、キューワーカー停止のいずれか。Debug consoleにイベントが届いているかで、サーバー側とクライアント側を切り分ける。 - whisperだけ届かない:ダッシュボードでClient Eventsが無効、または公開チャンネル(
Echo.channel())で送っている。 - まれに古い内容や空のデータが届く:トランザクションのコミット前に配信されている。イベントに
ShouldDispatchAfterCommitを付ける。 - 同じメッセージが2回表示される:送信者側で
toOthers()を付けていない、またはSPAの画面遷移で購読を重ねている。 - 403が返る:
routes/channels.phpの認可がfalseを返している、または未ログイン。 - エラーコード4004で切られる:プランの同時接続上限に達している。
- 4100番台で切れて再接続を繰り返す:プロトコル仕様上は1秒以上待って再接続する区分。頻発するならstatus.pusher.comで稼働状況を確認する。
よくある質問
Pusherの読み方は?
「プッシャー」です。英語のpush(押す)に由来し、サーバーからクライアントへデータを押し出す動作を表しています。開発の文脈では、Pusher社のリアルタイム通信サービス(主にChannels)を指します。
Pusherは無料で使える?
Sandboxプランが無料で、100同時接続・1日20万メッセージまで使えます。開発・検証・社内ツールならこのまま運用できます。上限を超えた接続は拒否されるので、一般公開のサービスではピーク時の接続数とメッセージ数を見積もり、無料枠を超える場合は有料プラン(Startup 月49ドル〜)を選びます。
BROADCAST_DRIVERを設定しても動かないのはなぜ?
Laravel 11以降、この環境変数は BROADCAST_CONNECTION に改名されました。config/broadcasting.php は env('BROADCAST_CONNECTION', 'null') を読むため、古い名前だけを書くと null ドライバーが選ばれ、イベントはどこにも送られません。.env を書き換えたうえで php artisan config:clear を実行してください。
PUSHER_APP_IDやPUSHER_APP_KEYはどこで確認する?
Pusherのダッシュボードで、Channelsのアプリを開いた「App Keys」タブに app_id・key・secret・cluster の4つが並んでいます。.env の PUSHER_APP_ID・PUSHER_APP_KEY・PUSHER_APP_SECRET・PUSHER_APP_CLUSTER にそれぞれ写します。フロントに渡すのはkeyとclusterだけです。
PusherからReverbへ移行するとコードはどれだけ変わる?
ReverbはPusherプロトコル互換のため、イベントクラス・routes/channels.php・listen() の書き方は変わりません。変更は .env の接続設定と echo.js の broadcaster: 'reverb'・wsHost 指定、そしてサーバーで php artisan reverb:start を常駐させる運用の追加です。Reverbも内部でPusherプロトコルのクライアントを使うため pusher-js は残ります。暗号化プライベートチャンネルを使っている機能だけは、移行前に代わりの設計が要ります。