WebSockets and Real-Time Apps 2026: Socket.io, SSE, and Pusher Guide

Sanjeev SharmaSanjeev Sharma
5 min read

Advertisement

Real-Time in 2026: Pick the Right Protocol

ProtocolUse CaseConnectionServer Load
WebSocketChat, games, collaborationPersistentHigh
SSELive feeds, dashboardsPersistent (one-way)Medium
Long PollingSimple notificationsPollingLow
Pusher/AblyServerless real-timeManagedNone

WebSocket Chat with Socket.io

npm install socket.io socket.io-client
// server.ts — Socket.io server
import { createServer } from 'http'
import { Server } from 'socket.io'
import express from 'express'

const app = express()
const httpServer = createServer(app)

const io = new Server(httpServer, {
  cors: { origin: process.env.CLIENT_URL, credentials: true },
  transports: ['websocket', 'polling'],
})

// Types
interface ChatMessage {
  id: string
  roomId: string
  userId: string
  username: string
  content: string
  timestamp: number
}

// Middleware: authenticate socket connection
io.use(async (socket, next) => {
  const token = socket.handshake.auth.token
  try {
    const user = verifyToken(token)
    socket.data.user = user
    next()
  } catch {
    next(new Error('Unauthorized'))
  }
})

io.on('connection', (socket) => {
  const user = socket.data.user
  console.log(`${user.name} connected`)

  // Join a room
  socket.on('join_room', async (roomId: string) => {
    socket.join(roomId)

    // Load recent messages
    const messages = await getRecentMessages(roomId, 50)
    socket.emit('message_history', messages)

    // Notify room of new user
    socket.to(roomId).emit('user_joined', { userId: user.id, username: user.name })

    // Track presence
    const members = await getActiveMembers(roomId)
    io.to(roomId).emit('presence_update', members)
  })

  socket.on('send_message', async (data: { roomId: string; content: string }) => {
    const message: ChatMessage = {
      id: crypto.randomUUID(),
      roomId: data.roomId,
      userId: user.id,
      username: user.name,
      content: data.content,
      timestamp: Date.now(),
    }

    // Save to DB
    await saveMessage(message)

    // Broadcast to all in room (including sender)
    io.to(data.roomId).emit('new_message', message)
  })

  socket.on('typing', (roomId: string) => {
    socket.to(roomId).emit('user_typing', { userId: user.id, username: user.name })
  })

  socket.on('disconnect', () => {
    console.log(`${user.name} disconnected`)
    // Update presence in all rooms
  })
})

httpServer.listen(3001)

React Client with Socket.io

// hooks/use-chat.ts
'use client'
import { useEffect, useRef, useState, useCallback } from 'react'
import { io, Socket } from 'socket.io-client'

interface Message {
  id: string
  userId: string
  username: string
  content: string
  timestamp: number
}

export function useChat(roomId: string) {
  const socketRef = useRef<Socket | null>(null)
  const [messages, setMessages] = useState<Message[]>([])
  const [isConnected, setIsConnected] = useState(false)
  const [typingUsers, setTypingUsers] = useState<string[]>([])

  useEffect(() => {
    const token = getAuthToken()
    const socket = io(process.env.NEXT_PUBLIC_SOCKET_URL!, {
      auth: { token },
      transports: ['websocket'],
    })

    socketRef.current = socket

    socket.on('connect', () => setIsConnected(true))
    socket.on('disconnect', () => setIsConnected(false))

    socket.on('message_history', (history: Message[]) => {
      setMessages(history)
    })

    socket.on('new_message', (message: Message) => {
      setMessages(prev => [...prev, message])
    })

    socket.on('user_typing', ({ username }: { username: string }) => {
      setTypingUsers(prev => [...new Set([...prev, username])])
      setTimeout(() => {
        setTypingUsers(prev => prev.filter(u => u !== username))
      }, 3000)
    })

    socket.emit('join_room', roomId)

    return () => {
      socket.disconnect()
    }
  }, [roomId])

  const sendMessage = useCallback((content: string) => {
    socketRef.current?.emit('send_message', { roomId, content })
  }, [roomId])

  const notifyTyping = useCallback(() => {
    socketRef.current?.emit('typing', roomId)
  }, [roomId])

  return { messages, isConnected, typingUsers, sendMessage, notifyTyping }
}

// Chat UI
export function ChatRoom({ roomId }: { roomId: string }) {
  const { messages, isConnected, typingUsers, sendMessage, notifyTyping } = useChat(roomId)
  const [input, setInput] = useState('')
  const messagesEndRef = useRef<HTMLDivElement>(null)

  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
  }, [messages])

  return (
    <div className="flex flex-col h-full">
      <div className="flex-1 overflow-y-auto p-4 space-y-2">
        {messages.map(msg => (
          <div key={msg.id} className="flex gap-2">
            <span className="font-bold text-blue-600">{msg.username}</span>
            <span>{msg.content}</span>
          </div>
        ))}
        {typingUsers.length > 0 && (
          <div className="text-gray-500 text-sm italic">
            {typingUsers.join(', ')} {typingUsers.length === 1 ? 'is' : 'are'} typing...
          </div>
        )}
        <div ref={messagesEndRef} />
      </div>

      <form
        onSubmit={(e) => { e.preventDefault(); if (input.trim()) { sendMessage(input); setInput('') } }}
        className="p-4 flex gap-2"
      >
        <input
          value={input}
          onChange={(e) => { setInput(e.target.value); notifyTyping() }}
          placeholder="Type a message..."
          className="flex-1 border rounded px-3 py-2"
        />
        <button type="submit" className="bg-blue-500 text-white px-4 py-2 rounded">Send</button>
      </form>
    </div>
  )
}

Server-Sent Events (SSE): Live Dashboards

SSE is perfect for one-way streams (server to client) — no overhead of full WebSocket:

// app/api/events/route.ts — Next.js SSE endpoint
export async function GET(request: Request) {
  const encoder = new TextEncoder()

  const stream = new ReadableStream({
    start(controller) {
      // Send initial data
      const send = (data: object) => {
        controller.enqueue(
          encoder.encode(`data: ${JSON.stringify(data)}\n\n`)
        )
      }

      send({ type: 'connected', timestamp: Date.now() })

      // Interval updates
      const interval = setInterval(async () => {
        const stats = await getRealtimeStats()
        send({ type: 'stats', data: stats })
      }, 1000)

      // Subscribe to Redis events
      const redisSubscriber = redis.duplicate()
      redisSubscriber.subscribe('dashboard:updates')
      redisSubscriber.on('message', (_, message) => {
        send({ type: 'update', data: JSON.parse(message) })
      })

      // Cleanup on disconnect
      request.signal.addEventListener('abort', () => {
        clearInterval(interval)
        redisSubscriber.unsubscribe()
        redisSubscriber.quit()
        controller.close()
      })
    },
  })

  return new Response(stream, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      'Connection': 'keep-alive',
    },
  })
}
// React hook for SSE
function useLiveDashboard() {
  const [stats, setStats] = useState<Stats | null>(null)

  useEffect(() => {
    const eventSource = new EventSource('/api/events')

    eventSource.onmessage = (event) => {
      const data = JSON.parse(event.data)
      if (data.type === 'stats') setStats(data.data)
    }

    eventSource.onerror = () => eventSource.close()

    return () => eventSource.close()
  }, [])

  return stats
}

Scale with Redis Adapter

// Multiple Node.js instances need Redis to share Socket.io state
import { createAdapter } from '@socket.io/redis-adapter'
import Redis from 'ioredis'

const pubClient = new Redis(process.env.REDIS_URL!)
const subClient = pubClient.duplicate()

io.adapter(createAdapter(pubClient, subClient))

// Now messages broadcast across all Node.js instances
// io.to(roomId).emit('message', data)  → works across servers

Real-time features are table stakes for modern apps. Pick WebSocket for bidirectional communication, SSE for feeds and dashboards, Pusher for serverless.

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro