Skip to main content

Best npm Packages for Real-Time Features 2026

·PkgPulse Team
0

TL;DR

Socket.io is still the easiest WebSocket solution for Node.js servers. PartyKit is the 2026 edge-native option (Cloudflare Durable Objects — no server management). Yjs is essential for collaborative editing (conflict-free replicated data types). For simple chat/notifications: Socket.io or Ably. For Google Docs-like collaborative editing: Yjs + PartyKit or y-websocket. The 2026 real-time landscape has diverged cleanly into two camps: self-hosted (Socket.io, y-websocket) and managed/serverless (PartyKit, Ably).

Key Takeaways

  • Socket.io: ~9M downloads/week — rooms, namespaces, fallback transports, Node.js native, Redis adapter for multi-server
  • PartyKit — Cloudflare Durable Objects abstraction, persistent WebSocket rooms, zero server management
  • Yjs: ~600K downloads/week — CRDT library for conflict-free collaborative editing, works with any transport
  • Ably: ~1M downloads/week — managed pub/sub, guaranteed delivery, 6M free messages/month
  • 2026 pattern: PartyKit + Yjs for collaborative apps; Socket.io for chat/events on self-hosted Node.js

The Real-Time Landscape in 2026

Adding real-time features to a web application used to mean one of two things: polling an endpoint every few seconds (simple but wasteful) or setting up a WebSocket server (correct but operationally complex). WebSocket infrastructure introduces stateful server connections, which breaks the stateless deployment model that made serverless and edge computing so appealing.

The 2025-2026 shift has been the emergence of hosted WebSocket infrastructure that abstracts away the stateful server problem. Cloudflare Durable Objects give PartyKit persistent state at the edge. Ably provides a global managed pub/sub network. These tools let teams add real-time features without running WebSocket servers.

At the same time, Socket.io remains the most practical solution for teams already running Node.js servers who want to add WebSocket functionality without introducing external dependencies. And Yjs has become the standard library for collaborative editing regardless of which transport you use underneath.

Understanding which layer of the stack each tool addresses is the key to combining them correctly.


Socket.io — The Standard WebSocket (~9M downloads)

Socket.io is the most downloaded WebSocket library for Node.js by a significant margin. Its 9 million weekly downloads reflect years of use as the default choice for chat applications, live notifications, dashboards, and multiplayer features on traditional Node.js stacks.

Socket.io's core abstractions — rooms, namespaces, and automatic reconnection — handle the most common patterns in real-time applications. A room is a named channel that connections can join and leave. A namespace is a logical partition of the Socket.io server (like separate paths for different parts of an application). Automatic reconnection means clients re-establish their connection without any application code if the server restarts or the network drops.

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

const httpServer = createServer(app);

const io = new Server(httpServer, {
  cors: {
    origin: process.env.FRONTEND_URL,
    credentials: true,
  },
  transports: ['websocket', 'polling'],  // Polling as fallback
});

// Middleware for authentication
io.use(async (socket, next) => {
  const token = socket.handshake.auth.token;
  const user = await verifyToken(token);
  if (!user) return next(new Error('Unauthorized'));
  socket.data.userId = user.id;
  socket.data.userName = user.name;
  next();
});

io.on('connection', (socket) => {
  const { userId, userName } = socket.data;

  // Automatically join user's private room
  socket.join(`user:${userId}`);

  socket.on('join-channel', (channelId: string) => {
    socket.join(`channel:${channelId}`);
    io.to(`channel:${channelId}`).emit('user-joined', { userId, userName });
  });

  socket.on('leave-channel', (channelId: string) => {
    socket.leave(`channel:${channelId}`);
    io.to(`channel:${channelId}`).emit('user-left', { userId });
  });

  socket.on('send-message', async (data: { channelId: string; content: string }) => {
    // Persist to database first
    const message = await db.message.create({
      data: {
        channelId: data.channelId,
        senderId: userId,
        content: data.content,
      },
      include: { sender: { select: { id: true, name: true } } },
    });

    // Broadcast to everyone in the channel
    io.to(`channel:${data.channelId}`).emit('new-message', message);
  });

  socket.on('typing', (channelId: string) => {
    // Broadcast to everyone except sender
    socket.to(`channel:${channelId}`).emit('user-typing', { userId, userName });
  });

  socket.on('disconnect', () => {
    // Notify relevant rooms
    io.emit('user-offline', { userId });
  });
});

httpServer.listen(3001);
// React client with Socket.io
import { io, Socket } from 'socket.io-client';
import { useEffect, useRef, useState } from 'react';

function useChatSocket(channelId: string, token: string) {
  const socketRef = useRef<Socket | null>(null);
  const [messages, setMessages] = useState<Message[]>([]);
  const [typingUsers, setTypingUsers] = useState<string[]>([]);

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

    socket.on('connect', () => {
      socket.emit('join-channel', channelId);
    });

    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));
      }, 2000);
    });

    socketRef.current = socket;
    return () => {
      socket.emit('leave-channel', channelId);
      socket.disconnect();
    };
  }, [channelId, token]);

  const sendMessage = (content: string) => {
    socketRef.current?.emit('send-message', { channelId, content });
  };

  const sendTyping = () => {
    socketRef.current?.emit('typing', channelId);
  };

  return { messages, sendMessage, sendTyping, typingUsers };
}

Socket.io's binary support is worth noting for applications that transmit images, audio, or file data over WebSockets. You can emit Buffer objects directly, and Socket.io handles the binary framing. This avoids base64 encoding overhead that would otherwise double the size of binary payloads.

Multi-server scaling is Socket.io's main operational consideration. A single Node.js process has a connection limit, and sticky sessions are required when you run multiple instances behind a load balancer. The Redis adapter handles this:

import { createAdapter } from '@socket.io/redis-adapter';
import { createClient } from 'redis';

const pubClient = createClient({ url: process.env.REDIS_URL });
const subClient = pubClient.duplicate();

await Promise.all([pubClient.connect(), subClient.connect()]);
io.adapter(createAdapter(pubClient, subClient));

With the Redis adapter, emit events from any server instance and they fan out to all connected clients regardless of which server they are connected to.


PartyKit — Edge-Native WebSockets

PartyKit is built on Cloudflare Durable Objects, which are stateful serverless workers with persistent memory. Each "party" (room) is a Durable Object instance — a persistent WebSocket server that lives at the edge closest to the users connecting to it. There is no Node.js server to provision, no WebSocket connection management, and no Redis adapter needed for multi-server state.

The model is: write a server class that handles connections and messages, deploy it with partykit deploy, and the infrastructure handles routing, scaling, persistence, and geographic distribution.

npm install partykit partysocket

# Local development
npx partykit dev

# Deploy to Cloudflare
npx partykit deploy
// party/chatroom.ts — PartyKit server
import type * as Party from 'partykit/server';

interface Message {
  type: 'message' | 'join' | 'leave' | 'history';
  content?: string;
  user: string;
  timestamp: string;
  messages?: Message[];
}

export default class ChatRoom implements Party.Server {
  messages: Message[] = [];

  constructor(readonly room: Party.Room) {}

  async onConnect(conn: Party.Connection, ctx: Party.ConnectionContext) {
    const url = new URL(ctx.request.url);
    const user = url.searchParams.get('user') ?? 'Anonymous';

    // Load persisted history from Durable Object storage
    const stored = await this.room.storage.get<Message[]>('messages');
    if (stored) this.messages = stored;

    // Send history to the new connection
    conn.send(
      JSON.stringify({ type: 'history', messages: this.messages, user, timestamp: '' })
    );

    // Announce join to everyone else
    this.room.broadcast(
      JSON.stringify({ type: 'join', user, timestamp: new Date().toISOString() }),
      [conn.id]
    );
  }

  async onMessage(raw: string, sender: Party.Connection) {
    const { content, user } = JSON.parse(raw);

    const msg: Message = {
      type: 'message',
      content,
      user,
      timestamp: new Date().toISOString(),
    };

    // Keep last 100 messages
    this.messages = [...this.messages.slice(-99), msg];

    // Persist to Durable Object storage
    await this.room.storage.put('messages', this.messages);

    // Broadcast to all connections in the room
    this.room.broadcast(JSON.stringify(msg));
  }

  onClose(conn: Party.Connection) {
    // Cleanup if needed
  }
}
// Client-side with PartySocket
import PartySocket from 'partysocket';
import { useEffect, useRef, useState } from 'react';

function usePartyChat(roomId: string, userName: string) {
  const socketRef = useRef<PartySocket | null>(null);
  const [messages, setMessages] = useState<Message[]>([]);

  useEffect(() => {
    const socket = new PartySocket({
      host: process.env.NEXT_PUBLIC_PARTYKIT_HOST!,
      room: roomId,
      query: { user: userName },
    });

    socket.onmessage = (event) => {
      const data = JSON.parse(event.data) as Message;
      if (data.type === 'history' && data.messages) {
        setMessages(data.messages);
      } else if (data.type === 'message') {
        setMessages(prev => [...prev, data]);
      }
    };

    socketRef.current = socket;
    return () => socket.close();
  }, [roomId, userName]);

  const send = (content: string) => {
    socketRef.current?.send(JSON.stringify({ content, user: userName }));
  };

  return { messages, send };
}

PartyKit's advantage over Socket.io is operational: there is no server to provision, no Redis adapter for scaling, and the Durable Object model means state is persistent by default. Each room persists its state to Cloudflare's storage, survives server restarts, and reconnects automatically.

The limitation is the Cloudflare ecosystem dependency. PartyKit deploys to Cloudflare Workers — you cannot self-host it on your own infrastructure. For teams already on Cloudflare, this is ideal. For teams on AWS or GCP, it means introducing a Cloudflare dependency for one feature.


Yjs — Collaborative Editing with CRDTs (~600K downloads)

Yjs is not a WebSocket library — it is a conflict-free replicated data type (CRDT) library. A CRDT is a data structure designed so that multiple users can make changes simultaneously, and those changes can always be merged without conflicts, regardless of the order they are received.

This is the technology behind Google Docs, Notion, and Figma's collaborative editing. When two users edit the same document simultaneously, the CRDT ensures their changes merge correctly rather than one overwriting the other. There are no "merge conflicts" in the code editor sense — the algorithm is mathematically guaranteed to converge to the same state on all clients.

Yjs provides shared data types: Y.Text for text documents, Y.Map for key-value structures, Y.Array for ordered lists, and Y.XmlFragment for structured document trees. You connect these to your editor or data model, and Yjs handles the synchronization.

npm install yjs y-websocket
// Collaborative text editor with Yjs + y-websocket
import * as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';
import { useEditor, EditorContent } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import Collaboration from '@tiptap/extension-collaboration';
import CollaborationCursor from '@tiptap/extension-collaboration-cursor';
import { useMemo, useEffect } from 'react';

interface CollaborativeEditorProps {
  documentId: string;
  userId: string;
  userName: string;
  userColor: string;
}

export function CollaborativeEditor({
  documentId,
  userId,
  userName,
  userColor,
}: CollaborativeEditorProps) {
  const ydoc = useMemo(() => new Y.Doc(), [documentId]);

  const provider = useMemo(
    () =>
      new WebsocketProvider(
        process.env.NEXT_PUBLIC_YJS_SERVER_URL!,
        `document-${documentId}`,
        ydoc
      ),
    [documentId, ydoc]
  );

  const editor = useEditor({
    extensions: [
      StarterKit.configure({ history: false }), // Yjs handles history
      Collaboration.configure({ document: ydoc }),
      CollaborationCursor.configure({
        provider,
        user: { name: userName, color: userColor },
      }),
    ],
  });

  useEffect(() => {
    return () => {
      provider.disconnect();
      ydoc.destroy();
    };
  }, [provider, ydoc]);

  return <EditorContent editor={editor} />;
}
// Y.Map for collaborative presence / shared state
import * as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';

const ydoc = new Y.Doc();
const provider = new WebsocketProvider(WS_URL, 'room-' + roomId, ydoc);

// Shared cursor positions map
const awareness = provider.awareness;

// Set own cursor position
awareness.setLocalStateField('cursor', {
  user: { name: userName, color: '#2563eb' },
  position: { x: 100, y: 200 },
});

// React to other users' state
awareness.on('change', () => {
  const states = Array.from(awareness.getStates().entries());
  const cursors = states
    .filter(([clientId]) => clientId !== ydoc.clientID)
    .map(([, state]) => state.cursor)
    .filter(Boolean);
  renderCursors(cursors);
});

Yjs supports multiple network providers, which means you can swap the transport without changing any application code. Use y-websocket for a self-hosted Node.js server, y-partykit for PartyKit's Durable Objects, or y-webrtc for peer-to-peer collaboration without a server at all.

The y-webrtc provider is worth knowing about for specific use cases: it connects clients directly peer-to-peer using WebRTC data channels, with a lightweight signaling server to establish the initial connection. Once connected, no server is involved in data exchange. This makes it free to operate at any scale, with the trade-off that WebRTC connectivity is less reliable than server-based WebSockets in restricted network environments (corporate firewalls, strict NAT configurations).

Yjs's offline support is another capability that sets it apart from event-based real-time systems. A Y.Doc accumulates changes locally when offline. When the network reconnects, it syncs all accumulated changes with remote peers, and the CRDT algorithm guarantees correct merging even when those offline changes interleave with changes made by other users during the disconnection. This is the same model that powers Figma's offline editing — you can close your laptop, make changes on the train, and open it back up to see your changes merge seamlessly with your team's work.

# Self-host the y-websocket server
npx y-websocket-server --host 0.0.0.0 --port 1234

# Or use y-partykit for serverless Yjs
npm install y-partykit

Ably — Managed Pub/Sub (~1M downloads)

Ably is a managed real-time messaging platform. Instead of running WebSocket servers, you publish messages to Ably's global network and subscribe to channels. Ably handles delivery guarantees, message ordering, connection management, and geographic distribution.

The npm package integrates with Ably's hosted infrastructure:

npm install ably
// Ably — publish/subscribe
import Ably from 'ably';

// Initialize with API key (server-side) or JWT (client-side for security)
const client = new Ably.Realtime({
  key: process.env.ABLY_API_KEY,
  // For production: use token auth
  authUrl: '/api/ably-token',
});

// Subscribe to a channel
const channel = client.channels.get('notifications:user-123');

channel.subscribe('order-update', (message) => {
  console.log('Order update:', message.data);
  updateOrderStatus(message.data);
});

// Publish a message (server-side)
await channel.publish('order-update', {
  orderId: 'ord_123',
  status: 'shipped',
  trackingNumber: '1Z999AA10123456784',
});
// Ably — presence (who is online)
const channel = client.channels.get('workspace:abc');

// Enter presence
await channel.presence.enter({ userId: currentUser.id, name: currentUser.name });

// Get current members
const members = await channel.presence.get();
console.log('Online:', members.map(m => m.data.name));

// Subscribe to presence changes
channel.presence.subscribe('enter', (member) => {
  console.log(`${member.data.name} came online`);
});

channel.presence.subscribe('leave', (member) => {
  console.log(`${member.data.name} went offline`);
});

Ably's free tier covers 6 million messages per month, which is enough for small applications and development. Pricing scales with message volume beyond that. The value proposition is zero WebSocket infrastructure: no servers, no Redis, no ops.

The limitation is the dependency on Ably's infrastructure. If Ably has an outage, your real-time features go down. For most applications this is acceptable (uptime SLAs are strong), but for applications where real-time is the core product, some teams prefer the control of self-hosted Socket.io.


Package Health

PackageWeekly DownloadsArchitectureHosted vs Self-hostedCRDTFree Tier
socket.io~9MNode.js serverSelf-hostedOpen source
yjs~600KLibrary (transport-agnostic)Self-hosted or PartyKitOpen source
ably~1MManaged serviceHosted6M msgs/month
partykit~50KCloudflare edgeHosted (Cloudflare)❌ (combine with Yjs)1B msgs/month

Comparison Table

Socket.ioPartyKitYjsAbly
Downloads~9M/week~50K/week~600K/week~1M/week
InfrastructureSelf-hostedCloudflare edgeSelf-hosted or PartyKitFully managed
CRDT / collaborative editing❌ (use with Yjs)✅ Core feature
Rooms / channels✅ Native✅ PartiesVia awareness✅ Channels
PresenceManualConnections in roomAwareness protocol✅ Built-in
Message persistenceManual (DB)Durable Object storageCRDT state72h history
Multi-server scalingRedis adapterAutomaticAutomaticAutomatic
LatencyYour serverEdge (Cloudflare)Your transportGlobal PoPs
TypeScript

When to Choose

Socket.io is the right choice when you are already running a Node.js server (Express, Fastify, Hono) and want to add WebSocket functionality without introducing external managed services. The rooms and namespaces model handles chat, notifications, live dashboards, and multiplayer features cleanly. Add the Redis adapter when you need to run more than one server instance.

PartyKit is the right choice when you are building on Cloudflare Workers or want to add real-time features to a serverless application without managing WebSocket infrastructure. Each party (room) is a persistent Durable Object that survives at the edge, automatically close to your users, with no ops overhead.

Yjs is the right choice whenever you need collaborative editing — not as an alternative to the others, but as a layer on top of them. Yjs handles the conflict-free data model; you provide the transport (y-websocket over Socket.io, y-partykit over PartyKit, or y-webrtc for peer-to-peer). The combination of Yjs + PartyKit has become the 2026 standard for Google Docs-style collaboration in web apps.

Ably is the right choice when you want zero WebSocket infrastructure and are comfortable with a managed service dependency. The 6M free message tier is generous enough for development and small applications. Ably's pub/sub model, presence, and guaranteed delivery make it practical for fan-out use cases (notify thousands of clients) where self-hosted Socket.io would require careful capacity planning.

ScenarioTool
Chat app on Node.js serverSocket.io
Live notifications, self-hostedSocket.io
Serverless/Cloudflare real-timePartyKit
Collaborative text editorYjs + y-websocket or y-partykit
Collaborative whiteboard/drawingYjs
Managed pub/sub, no infrastructureAbly
Fan-out to thousands of clientsAbly
Google Docs-like featuresYjs + PartyKit
Multi-server Node.js appSocket.io + Redis adapter

The real-time ecosystem in 2026 has matured to the point where each tool has a clear use case. Socket.io remains the default for self-hosted Node.js applications. PartyKit eliminates the server management problem for Cloudflare deployments. Yjs is the standard library for any collaborative editing feature. And Ably is the managed service for teams that want real-time without any infrastructure.


The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.