Fix: Pusher Not Working — Events Not Received, Channel Auth Failing, or Connection Dropping
Quick Answer
How to fix Pusher real-time issues — client and server setup, channel types, presence channels, authentication endpoints, event binding, connection management, and React integration.
The Problem
Events are triggered on the server but the client doesn’t receive them:
// Server
pusher.trigger('my-channel', 'my-event', { message: 'hello' });
// Client
channel.bind('my-event', (data) => {
console.log(data); // Never fires
});Or subscribing to a private channel fails:
Pusher: Error: No callbacks on subscriptions for pusher:subscription_errorOr the connection drops and doesn’t reconnect:
Pusher: Connection state: disconnected
Pusher: Connection state: unavailableWhy This Happens
Pusher is a hosted WebSocket service for real-time communication. Issues typically come from:
- App credentials are separate for client and server — the client uses the
key(public) and the server uses theappId,key,secret, andcluster. Using the wrong cluster or key on either side means messages go to the wrong app. - Private and presence channels require server-side authentication — channels starting with
private-orpresence-need an auth endpoint. The client sends a request to your server, which signs the subscription with the Pusher secret. Without this endpoint, subscription fails. - The cluster must match — Pusher apps are region-specific (
us2,eu,ap1, etc.). If the client connects tous2but the server triggers oneu, events are sent to different clusters. - Events must match exactly —
channel.bind('my-event', ...)only receives events named exactlymy-event. A server triggeringmyEvent(no hyphen) doesn’t match.
Fix 1: Server Setup
npm install pusher # Server
npm install pusher-js # Client// lib/pusher-server.ts
import Pusher from 'pusher';
export const pusher = new Pusher({
appId: process.env.PUSHER_APP_ID!,
key: process.env.PUSHER_KEY!,
secret: process.env.PUSHER_SECRET!,
cluster: process.env.PUSHER_CLUSTER!, // e.g., 'us2', 'eu', 'ap1'
useTLS: true,
});
// Trigger an event
export async function sendNotification(userId: string, message: string) {
await pusher.trigger(`private-user-${userId}`, 'notification', {
message,
timestamp: new Date().toISOString(),
});
}
// Trigger to multiple channels
export async function broadcastMessage(channelIds: string[], data: any) {
// Max 10 channels per trigger call
const batches = [];
for (let i = 0; i < channelIds.length; i += 10) {
batches.push(channelIds.slice(i, i + 10));
}
for (const batch of batches) {
await pusher.trigger(batch, 'new-message', data);
}
}// lib/pusher-client.ts
import PusherClient from 'pusher-js';
export const pusherClient = new PusherClient(
process.env.NEXT_PUBLIC_PUSHER_KEY!,
{
cluster: process.env.NEXT_PUBLIC_PUSHER_CLUSTER!,
// Auth endpoint for private/presence channels
channelAuthorization: {
endpoint: '/api/pusher/auth',
transport: 'ajax',
},
},
);Fix 2: Channel Authentication Endpoint
// app/api/pusher/auth/route.ts — authenticate private/presence channels
import { pusher } from '@/lib/pusher-server';
import { auth } from '@/auth';
export async function POST(req: Request) {
const session = await auth();
if (!session?.user) {
return Response.json({ error: 'Unauthorized' }, { status: 403 });
}
const body = await req.text();
const params = new URLSearchParams(body);
const socketId = params.get('socket_id')!;
const channelName = params.get('channel_name')!;
// Verify the user should access this channel
if (channelName.startsWith('private-user-')) {
const channelUserId = channelName.replace('private-user-', '');
if (channelUserId !== session.user.id) {
return Response.json({ error: 'Forbidden' }, { status: 403 });
}
}
// For presence channels — include user data
if (channelName.startsWith('presence-')) {
const presenceData = {
user_id: session.user.id,
user_info: {
name: session.user.name,
avatar: session.user.image,
},
};
const authResponse = pusher.authorizeChannel(socketId, channelName, presenceData);
return Response.json(authResponse);
}
// For private channels
const authResponse = pusher.authorizeChannel(socketId, channelName);
return Response.json(authResponse);
}Fix 3: React Integration
// hooks/usePusher.ts — custom hook
'use client';
import { useEffect, useRef, useState } from 'react';
import { pusherClient } from '@/lib/pusher-client';
import type { Channel, PresenceChannel } from 'pusher-js';
// Subscribe to a channel and bind events
export function useChannel(channelName: string) {
const [channel, setChannel] = useState<Channel | null>(null);
useEffect(() => {
const ch = pusherClient.subscribe(channelName);
setChannel(ch);
return () => {
pusherClient.unsubscribe(channelName);
};
}, [channelName]);
return channel;
}
// Bind to a specific event
export function useEvent<T = any>(
channel: Channel | null,
eventName: string,
callback: (data: T) => void,
) {
const callbackRef = useRef(callback);
callbackRef.current = callback;
useEffect(() => {
if (!channel) return;
const handler = (data: T) => callbackRef.current(data);
channel.bind(eventName, handler);
return () => {
channel.unbind(eventName, handler);
};
}, [channel, eventName]);
}
// Presence channel hook
export function usePresenceChannel(channelName: string) {
const [members, setMembers] = useState<Map<string, any>>(new Map());
const channelRef = useRef<PresenceChannel | null>(null);
useEffect(() => {
const channel = pusherClient.subscribe(channelName) as PresenceChannel;
channelRef.current = channel;
channel.bind('pusher:subscription_succeeded', (data: any) => {
setMembers(new Map(Object.entries(data.members)));
});
channel.bind('pusher:member_added', (member: any) => {
setMembers(prev => new Map(prev).set(member.id, member.info));
});
channel.bind('pusher:member_removed', (member: any) => {
setMembers(prev => {
const next = new Map(prev);
next.delete(member.id);
return next;
});
});
return () => {
pusherClient.unsubscribe(channelName);
};
}, [channelName]);
return { members, channel: channelRef.current };
}// components/Chat.tsx — real-time chat
'use client';
import { useChannel, useEvent } from '@/hooks/usePusher';
import { useState } from 'react';
interface Message {
id: string;
text: string;
userId: string;
userName: string;
timestamp: string;
}
function ChatRoom({ roomId }: { roomId: string }) {
const [messages, setMessages] = useState<Message[]>([]);
const channel = useChannel(`private-room-${roomId}`);
// Listen for new messages
useEvent<Message>(channel, 'new-message', (message) => {
setMessages(prev => [...prev, message]);
});
// Listen for message deletions
useEvent<{ messageId: string }>(channel, 'message-deleted', ({ messageId }) => {
setMessages(prev => prev.filter(m => m.id !== messageId));
});
async function sendMessage(text: string) {
await fetch('/api/messages', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ roomId, text }),
});
// Server triggers Pusher event — will come back through the subscription
}
return (
<div>
<div>
{messages.map(m => (
<div key={m.id}>
<strong>{m.userName}:</strong> {m.text}
</div>
))}
</div>
<input
onKeyDown={(e) => {
if (e.key === 'Enter') {
sendMessage(e.currentTarget.value);
e.currentTarget.value = '';
}
}}
placeholder="Type a message..."
/>
</div>
);
}Fix 4: Server-Side Event Triggering
// app/api/messages/route.ts
import { pusher } from '@/lib/pusher-server';
import { auth } from '@/auth';
export async function POST(req: Request) {
const session = await auth();
if (!session?.user) return Response.json({ error: 'Unauthorized' }, { status: 401 });
const { roomId, text } = await req.json();
// Save to database
const message = await db.insert(messages).values({
id: crypto.randomUUID(),
roomId,
text,
userId: session.user.id,
userName: session.user.name ?? 'Anonymous',
timestamp: new Date().toISOString(),
}).returning();
// Trigger Pusher event
await pusher.trigger(`private-room-${roomId}`, 'new-message', message[0]);
return Response.json(message[0]);
}
// Trigger from Server Actions
'use server';
export async function updateDocument(docId: string, content: string) {
await db.update(documents).set({ content }).where(eq(documents.id, docId));
// Notify all viewers
await pusher.trigger(`private-doc-${docId}`, 'content-updated', {
content,
updatedBy: (await auth())?.user?.id,
updatedAt: new Date().toISOString(),
});
}Fix 5: Connection Management
'use client';
import { pusherClient } from '@/lib/pusher-client';
import { useEffect, useState } from 'react';
function ConnectionStatus() {
const [state, setState] = useState(pusherClient.connection.state);
useEffect(() => {
function handleStateChange(states: { current: string }) {
setState(states.current);
}
pusherClient.connection.bind('state_change', handleStateChange);
return () => {
pusherClient.connection.unbind('state_change', handleStateChange);
};
}, []);
const colors: Record<string, string> = {
connected: 'green',
connecting: 'yellow',
disconnected: 'red',
unavailable: 'red',
failed: 'red',
};
return (
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
<div style={{
width: '8px', height: '8px', borderRadius: '50%',
backgroundColor: colors[state] || 'gray',
}} />
<span>{state}</span>
</div>
);
}Fix 6: Batch Events and Rate Limits
// Pusher limits: 10 channels per trigger, 10KB per event
// For larger payloads, send a reference and let clients fetch
// Instead of:
await pusher.trigger('channel', 'large-update', hugeDataObject); // May exceed 10KB
// Do this:
await pusher.trigger('channel', 'data-updated', {
type: 'document',
id: 'doc-123',
updatedAt: new Date().toISOString(),
});
// Client fetches the actual data via API when it receives the event
// Batch trigger — up to 10 events in one API call
await pusher.triggerBatch([
{ channel: 'private-user-1', name: 'notification', data: { message: 'Hello' } },
{ channel: 'private-user-2', name: 'notification', data: { message: 'World' } },
]);Still Not Working?
Events triggered but client doesn’t receive them — check that the channel name and event name match exactly between server and client. Also verify the cluster matches: PUSHER_CLUSTER on the server and NEXT_PUBLIC_PUSHER_CLUSTER on the client must be the same value (e.g., both us2).
Private channel subscription fails with 403 — the auth endpoint is rejecting the subscription. Check that your /api/pusher/auth route is accessible, that the user is authenticated, and that pusher.authorizeChannel() is called with the correct socketId and channelName.
Connection drops frequently — Pusher connections can drop due to network instability. The client auto-reconnects by default. If connections drop immediately, check that useTLS: true is set and that no firewall is blocking WebSocket connections on port 443. Enable debug logging: PusherClient.logToConsole = true.
Events received by the sender — by default, the triggering client also receives the event. To exclude the sender, pass the socket_id: pusher.trigger('channel', 'event', data, { socket_id: senderSocketId }). Get the socket ID from pusherClient.connection.socket_id on the client.
For related real-time issues, see Fix: Liveblocks Not Working and Fix: Supabase Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: pdf-lib Not Working — PDF Not Generating, Fonts Not Embedding, or Pages Blank
How to fix pdf-lib issues — creating PDFs from scratch, modifying existing PDFs, embedding fonts and images, form filling, merging documents, and browser and Node.js usage.
Fix: date-fns Not Working — Wrong Timezone Output, Invalid Date, or Locale Not Applied
How to fix date-fns issues — timezone handling with date-fns-tz, parseISO vs new Date, locale import and configuration, DST edge cases, v3 ESM migration, and common format pattern mistakes.
Fix: Zod Validation Not Working — safeParse Returns Wrong Error, transform Breaks Type, or discriminatedUnion Fails
How to fix Zod schema validation issues — parse vs safeParse, transform and preprocess, refine for cross-field validation, discriminatedUnion, error formatting, and common schema mistakes.
Fix: TypeScript Function Overload Error — No Overload Matches This Call
How to fix TypeScript function overload errors — overload signature compatibility, implementation signature, conditional types as alternatives, method overloads in classes, and common pitfalls.