The problem: Laravel Echo throws a connection failed state in the browser console, and no broadcast events reach the frontend even though the backend fires them successfully.
Environment: Laravel 11, Laravel Echo 1.x, Pusher JS 8.x, laravel-echo + pusher-js via npm
The Error
Pusher : : ["State changed", {"previous":"connecting","current":"failed"}]
Pusher is not defined
GET https://sockjs-us2.pusher.com/... net::ERR_CONNECTION_TIMED_OUT
Why It Happens
A "failed" state (not "unavailable", not "disconnected") specifically means the WebSocket handshake never even completed — Echo/Pusher couldn't establish a connection at all. This narrows down to a small set of causes:
1. Wrong or missing cluster value. Pusher requires the correct cluster (e.g. ap2, mt1) matching the region your Pusher app was created in — a mismatched or missing cluster means the JS client tries to connect to the wrong regional endpoint entirely.
2. window.Pusher never got attached. Since Laravel 11 scaffolding removed auto-imports for Echo, it's common to configure echo.js correctly but forget to explicitly assign window.Pusher = Pusher before instantiating Echo — without it, Echo's internal broadcaster can't find the Pusher client at all.
3. Firewall/corporate network blocking WebSocket ports. Pusher's WebSocket transport uses port 443 but falls back to SockJS over HTTP long-polling on restrictive networks — if that fallback is also blocked, the connection times out completely rather than degrading gracefully.
The Fix
First, confirm your resources/js/echo.js (or bootstrap.js) explicitly imports and attaches Pusher before creating the Echo instance:
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,
});
Then double-check your .env values match exactly what's shown in your Pusher dashboard — the cluster is the most commonly mismatched field:
PUSHER_APP_ID=1234567
PUSHER_APP_KEY=abcdef123456
PUSHER_APP_SECRET=your-secret
PUSHER_APP_CLUSTER=ap2
VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
Rebuild after any .env change, since Vite bakes these values in at build time, not runtime:
npm run build
If the cluster and key are both confirmed correct but it still fails specifically on a corporate or restricted network, test on a different network (mobile hotspot) to rule out a firewall blocking WebSocket/SockJS traffic entirely.
Step-by-Step
- Open your Pusher dashboard and copy the exact App Key and Cluster values.
- Update
.envwith matchingPUSHER_APP_CLUSTERand confirm theVITE_-prefixed mirror variables are also set. - In
echo.js/bootstrap.js, confirmwindow.Pusher = Pusheris set before thenew Echo({...})call. - Run
npm run build(env changes require a rebuild, not just a dev server restart). - Open the browser console and watch for the Pusher connection state change from "connecting" to "connected" (not "failed").
- If it still fails, test from a different network to rule out a firewall or corporate proxy blocking WebSocket ports.
Edge case: If you're testing locally over plain HTTP (not HTTPS) with forceTLS: true set, some browsers will silently block the mixed-content WebSocket upgrade without a clear console error — set forceTLS: false only for local HTTP development, and always true in production.
Still stuck with a failed connection after checking all this? Reach out on the Contact page and I'll help debug your Pusher setup.