Socket.IO, ws, Fastify, Hono and NestJS compared for WebSocket APIs in TypeScript, plus how Encore adds typed streaming endpoints and the databases and topics a real-time backend needs.
Real-time features in TypeScript usually run over WebSockets, the protocol standardized in RFC 6455, and the framework choice decides how connections are typed, grouped and scaled past one server. A single process can hold many connections, and the work grows with the second instance, when a message received on one server has to reach clients connected to another, and when messages have to be stored for clients that reconnect.
What to weigh when choosing:
Socket.IO adds an event protocol on top of WebSockets with rooms, acknowledgements, automatic reconnection and an HTTP long-polling fallback.
import { Server } from "socket.io";
const io = new Server(3000);
io.on("connection", (socket) => {
socket.on("join", (room: string) => socket.join(room));
socket.on("message", (room: string, text: string) => {
io.to(room).emit("message", text);
});
});
Clients must use a Socket.IO client, since the protocol is not plain WebSocket, and the adapter's broker is another service to run. With several nodes, the Socket.IO docs on using multiple nodes list sticky sessions, disabling HTTP long-polling or client-side load balancing as the ways to keep sessions working, and load balancers such as AWS's Application Load Balancer support sticky sessions for this.
ws is a fast, small WebSocket implementation for Node.js that many frameworks and plugins build on.
Routing, typing, heartbeats and broadcast are left to you.
The @fastify/websocket plugin, built on ws, lets a Fastify route accept WebSocket connections with the same hooks as HTTP routes.
Messages are untyped by default, and fan-out across instances is yours to build.
HonoHono's WebSocket helper exposes one API across Bun, Deno, Cloudflare Workers and Node.
upgradeWebSocket with onMessage, onOpen and onClose events.Each runtime has its own adapter and limits, so check the one you deploy to.
NestJS gateways use decorators such as @SubscribeMessage and support Socket.IO or ws as the underlying platform.
It brings the full NestJS module system, which is a lot of structure if real-time is the only thing you need.
Encore combines typed streaming endpoints with the infrastructure behind them through an infrastructure SDK for TypeScript, whose API layer includes streaming endpoints that run over WebSockets with typed messages in each direction. Whichever option above you pick, Encore can add the database and topics to the same app. The same service declares the Postgres database that stores history and a Pub/Sub topic for events other services react to, encore run starts Postgres locally with the topic, and a deploy provisions both in your own AWS or GCP account.
import { api, StreamInOut } from "encore.dev/api";
import { Subscription, Topic } from "encore.dev/pubsub";
import { SQLDatabase } from "encore.dev/storage/sqldb";
const db = new SQLDatabase("chat", { migrations: "./migrations" });
interface ChatMessage {
room: string;
user: string;
text: string;
}
// Carries posted messages to subscribers.
const posted = new Topic<ChatMessage>("chat-posted", {
deliveryGuarantee: "at-least-once",
});
// Streams connected to this instance.
const connected = new Set<StreamInOut<ChatMessage, ChatMessage>>();
export const chat = api.streamInOut<ChatMessage, ChatMessage>(
{ path: "/chat", expose: true },
async (stream) => {
connected.add(stream);
try {
for await (const msg of stream) {
await db.exec`
INSERT INTO messages (room, sender, body)
VALUES (${msg.room}, ${msg.user}, ${msg.text})`;
await posted.publish(msg);
}
} finally {
connected.delete(stream);
}
},
);
const _ = new Subscription(posted, "deliver", {
handler: async (msg) => {
for (const stream of connected) {
try {
await stream.send(msg);
} catch {
connected.delete(stream);
}
}
},
});
Pub/Sub subscriptions deliver each message to one instance of the subscribing service, so delivering to every instance takes a broadcast mechanism of its own, such as a cache channel or a per-instance subscription, and the snippet above shows the single-instance shape. Rooms and presence are yours to build. When Encore provisions the database and topic, the cloud access sits with Encore's IAM role, not the developer or agent.
If the backend already uses Encore's API layer, its streaming endpoints (api.streamInOut) cover the WebSocket side and the same service declares the database and topics; otherwise choose among the libraries above.
Socket.IO is the most complete option, with rooms, reconnection and fallbacks, while ws is the minimal, fast WebSocket library many others build on. Fastify and Hono add WebSocket routes to an HTTP framework, and NestJS offers gateways with decorators. Separately, Encore is an infrastructure SDK whose API layer includes typed streaming endpoints, and it declares the database and Pub/Sub topics a real-time backend uses in your own cloud.
Use Socket.IO when you want rooms, acknowledgements and automatic reconnection without writing them. Use plain WebSockets through ws or your framework when clients are not Socket.IO clients, or you want the smallest protocol surface.
Deploy to your cloud on AWS and Google Cloud
without managing infrastructure
Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.