Real-time Communication

Socket.IO

Socket.IO ist ein Echtzeit-Event-Framework auf Basis von WebSockets. Es bietet Rooms, Acknowledgements, automatische Wiederverbindungen und einen Polling-Fallback, sodass Sie sich auf Features statt auf die Verbindungsinfrastruktur konzentrieren können.

intermediate14 min readUpdated 16. Sept. 2026
server.ts
ts
// server.ts
import { Server } from "socket.io";

const io = new Server(httpServer, {
  cors: { origin: "https://app.example.com" },
});

io.use((socket, next) => {
  const user = verifyToken(socket.handshake.auth.token);
  if (!user) return next(new Error("unauthorized"));
  socket.data.user = user;
  next();
});

io.on("connection", (socket) => {
  socket.join(`user:${socket.data.user.id}`);

  socket.on("message:send", (payload, ack) => {
    io.to(payload.room).emit("message:new", payload);
    ack({ ok: true });
  });
});
Veröffentlicht
2010
Läuft auf
Node.js
Transport
WebSocket mit Polling-Fallback
Protokoll
Engine.IO
Skalierung mit
Redis adapter
Client
Browser und Node.js

Warum es wichtig ist

Was Socket.IO Ihnen von Tag eins an bietet

Rooms und Namespaces

Gruppieren Sie Sockets serverseitig und senden Sie Events an einen Room, einen Namespace oder an alle außer den Absender. Keine manuelle Verwaltung der Verbindungen, und es skaliert über Instanzen hinweg.

Integrierte Wiederverbindung

Der Client verbindet sich nach einem Verbindungsabbruch mit einem Backoff-Algorithmus neu und kann verpasste Pakete wiederherstellen, sodass mobile Netzwerke oder der Ruhezustand von Laptops die App nicht stören.

Events mit Acknowledgements

Jedes Emit kann einen Callback enthalten, sodass sich Request/Response über einen Socket wie ein Funktionsaufruf anfühlt und nicht wie ein Ratespiel.

Das Gesamtbild

Die drei Ebenen von Socket.IO

Eine Event-Ebene für Ihre Anwendung, eine Transport-Ebene, die mit HTTP-Polling startet, und eine Adapter-Ebene zur Skalierung über mehrere Instanzen hinweg.

Events

Emit

Alles ist ein benanntes Event. Server und Client senden und empfangen auf denselben Kanalnamen, und die Payloads sind einfaches JSON.

Transport

Upgrade

Die Verbindung beginnt als HTTP Long Polling und wird zu WebSocket upgegradet, sodass sie Proxies, alte Browser und restriktive Netzwerke übersteht.

Adapter

Scale

Ein Adapter broadcastet Events zwischen Server-Instanzen, was es ermöglicht, dass viele Pods eine einzige logische Echtzeit-Anwendung bedienen.

HTML5 auf einen Blick

Die Komponenten im Überblick

Events

socket.emit und socket.on bewegen benannte Payloads in beide Richtungen.

Rooms

socket.join(room) und io.to(room).emit(...) zielen auf eine Teilmenge ab.

Namespaces

Teilen Sie eine Verbindung in isolierte Kanäle wie /chat und /admin auf.

Reconnection

Automatischer Backoff-Reconnect plus optionale State Recovery.

Acknowledgements

Ein Callback bestätigt, dass der Server ein Event empfangen und verarbeitet hat.

Redis adapter

Teilen Sie Broadcasts zwischen Instanzen und nutzen Sie Sticky Sessions.

Ablauf

Eine Echtzeit-Nachricht

Vom Handshake bis zum Disconnect folgt jede Socket.IO-Interaktion demselben kurzen Pfad.

  1. 1

    Connect

    Der Client öffnet eine Verbindung und übermittelt seine Anmeldedaten im Handshake-Auth-Payload.

  2. 2

    Join rooms

    Der Server weist den Socket basierend auf dem authentifizierten Benutzer, dem Tenant und etwaigen Abonnements Rooms zu.

  3. 3

    Emit

    Ein Client sendet ein benanntes Event mit einem JSON-Payload und optional einem Callback für die Antwort.

  4. 4

    Broadcast

    Der Server verarbeitet das Event und sendet es an den relevanten Room, sodass jeder interessierte Socket es erhält.

  5. 5

    Disconnect

    Wenn die Verbindung geschlossen wird, verlässt der Socket automatisch alle seine Rooms und der Server räumt auf.

Der vollständige Leitfaden

Socket.IO: Alles was Sie wissen müssen

Was Socket.IO gegenüber WebSockets bietet

Socket.IO ist eine Bibliothek für echtzeitbasierte, ereignisgesteuerte Kommunikation zwischen einem Server und seinen Clients. Im Hintergrund nutzt sie das WebSocket-Protokoll, sofern möglich, und greift ansonsten auf HTTP Long Polling zurück. Auf diesem Transportlayer bietet Socket.IO ein kompaktes Anwendungsprotokoll: benannte Events, Rooms, Acknowledgements, automatische Wiederverbindungen und Heartbeats.

Genau diese zusätzliche Ebene ist der entscheidende Vorteil. Eine reine WebSocket-Verbindung ist lediglich ein Datenkanal und sonst nichts. Jede reale Anwendung müsste daher die gleichen Funktionen immer wieder neu implementieren: Wie gruppiere ich Verbindungen nach Chat-Rooms oder Tenants? Woher weiß ich, dass eine Nachricht empfangen wurde? Wie implementiere ich einen sauberen Reconnect? Wie broadcaste ich Nachrichten über mehrere Server hinweg? Socket.IO beantwortet diese Fragen einmalig in einer gut getesteten Bibliothek, sodass Sie Ihre Zeit in das Produkt investieren können.

Der Kompromiss besteht darin, dass Socket.IO kein reiner WebSocket ist. Es definiert einen eigenen Handshake und ein eigenes Paketformat auf Basis von Engine.IO, weshalb ein nativer WebSocket-Client keine Verbindung zu einem Socket.IO-Server herstellen kann. Beide Seiten müssen den Socket.IO-Client verwenden. Wenn Sie Interoperabilität mit beliebigen WebSocket-Clients benötigen, nutzen Sie stattdessen den WebSockets-Guide und die ws-Bibliothek.

Der Rest dieses Guides setzt voraus, dass Sie bereits mit dem Basis-Protokoll vertraut sind und nun die „Batteries-included“-Version nutzen möchten.

Server- und Client-Setup

Der Server wird an einen bestehenden HTTP-Server angehängt, in der Regel an denselben, der auch Ihre API bereitstellt. Das bedeutet: ein Port, ein TLS-Zertifikat und keine zusätzliche Infrastruktur.

import { Server } from "socket.io";
import { createServer } from "node:http";

const httpServer = createServer(app);

const io = new Server(httpServer, {
  cors: { origin: process.env.APP_ORIGIN, credentials: true },
});

io.on("connection", (socket) => {
  console.log("connected", socket.id);
});

httpServer.listen(3000);

Der Client verbindet sich über denselben Origin und authentifiziert sich über den Handshake.

import { io } from "socket.io-client";

const socket = io("https://api.example.com", {
  auth: { token: getAccessToken() },
  withCredentials: true,
});

socket.on("connect", () => console.log("connected", socket.id));
socket.on("disconnect", (reason) => console.log("closed", reason));

Die socket.id ist ein Identifier pro Verbindung. Sie ist nützlich für das Logging und um eine spezifische Verbindung anzusprechen. Da sie sich jedoch bei einer Neuverbindung ändert, sollte sie niemals als User-ID verwendet oder als dauerhafter Status gespeichert werden.

Events, Acknowledgements und Callbacks

Alles in Socket.IO ist ein benanntes Event, das ein JSON-Payload transportiert. Sowohl der Server als auch der Client rufen emit zum Senden und on zum Zuhören auf. Namen sind einfache Strings, daher empfiehlt es sich, eine Konvention zu wählen und beizubehalten — noun:verb wie message:send, message:new und presence:joined ist auf beiden Seiten gut lesbar.

Das Feature, das Socket.IO von einem einfachen Socket unterscheidet, ist das Acknowledgement. Wenn der Emitter einen Callback als letztes Argument übergibt, kann der Empfänger diesen aufrufen, um zu antworten. Dadurch wird das Emit zu einem Request/Response-Zyklus über dieselbe Verbindung.

// client
socket.emit("message:send", { room: "general", body: "hello" }, (ack) => {
  if (!ack.ok) showError(ack.error);
});

// server
socket.on("message:send", (payload, ack) => {
  if (!payload.body) return ack({ ok: false, error: "empty" });
  io.to(payload.room).emit("message:new", payload);
  ack({ ok: true, at: Date.now() });
});

Nutzen Sie Acknowledgements für jedes Event, dessen Ergebnis der Client kennen muss: das Erstellen eines Datensatzes, das Beitreten zu einem Raum oder das Absenden eines Formulars. Für reine Broadcasts, bei denen niemand auf eine Antwort wartet, ist ein einfaches emit völlig ausreichend. Ein Mittelweg ist socket.timeout(5000).emit(...), welches den Callback fehlschlagen lässt, wenn kein Acknowledgement rechtzeitig eintrifft.

Middleware und die Event-Pipeline

Socket.IO verfügt über zwei Middleware-Layer. Wenn man die Logik im richtigen Layer platziert, bleiben die Handler übersichtlich.

Connection-Middleware, die mit io.use oder namespace.use registriert wird, wird einmal pro Verbindung vor dem connection-Event ausgeführt. Hier gehören Authentifizierung, Tenant-Resolution und das Setup pro Verbindung hin. Die Middleware wird in der Reihenfolge der Registrierung ausgeführt, wobei jede Middleware next() aufruft, um fortzufahren, oder next(new Error(...)), um die Verbindung abzulehnen.

io.use((socket, next) => {
  const startedAt = Date.now();

  socket.on("disconnect", () => {
    metrics.observe("socket.duration", Date.now() - startedAt);
  });

  next();
});

Event-Middleware, die mit socket.use registriert wird, wird für jedes eingehende Event auf diesem Socket ausgeführt. Sie ist der ideale Ort für Validierung, Rate Limiting und strukturiertes Logging, da sie den Event-Namen und den Payload sieht, bevor ein Handler darauf zugreift.

const chat = io.of("/chat");

chat.use((socket, next) => {
  if (!socket.data.user) return next(new Error("unauthorized"));
  next();
});

chat.use((socket, next) => {
  socket.onAny((event, ...args) => {
    logger.info({ event, userId: socket.data.user.id, args });
  });
  next();
});

socket.onAny beobachtet jedes eingehende Event und socket.onAnyOutgoing beobachtet alles, was gesendet wird. Zusammen ermöglichen sie einen vollständigen Trace einer Verbindung, ohne dass ein einziger Handler angepasst werden muss. Middleware ist zudem Namespace-aware: Der /admin-Namespace kann beispielsweise eine andere Rolle verlangen, ohne /chat zu beeinflussen.

Rooms und Namespaces

Zwei Gruppierungsmechanismen sorgen dafür, dass Nachrichten gezielt zugestellt werden, anstatt sie an alle zu broadcasten.

Ein Room ist ein serverseitiges Label für eine Gruppe von Sockets. Jeder Socket kann jederzeit jedem beliebigen Room beitreten oder diesen verlassen, und ein Socket kann gleichzeitig in vielen Rooms sein. Wenn Sie ein Event an einen Room senden, erhalten nur dessen Mitglieder dieses Event.

io.on("connection", (socket) => {
  socket.join(`user:${socket.data.user.id}`);

  socket.on("room:join", (room, ack) => {
    socket.join(room);
    ack({ ok: true });
  });
});

Ein Namespace ist ein separater Kommunikationskanal unter einem Pfad, wie zum Beispiel /chat oder /admin. Namespaces haben ihre eigene middleware, ihre eigenen Connection-Handler und ihre eigenen Rooms. Nutzen Sie Namespaces, um Verantwortlichkeiten zu trennen, die überhaupt keine Events teilen sollten – etwa ein öffentlicher Chat-Namespace und ein interner Admin-Namespace –, anstatt sie zur Modellierung von Daten innerhalb eines einzelnen Features zu verwenden.

Rooms sind das eigentliche Arbeitstier. Modellieren Sie diese nach den Dingen, die für Ihre Nutzer relevant sind: eine Konversation, ein Dokument, ein Tenant oder ein Dashboard. So wird das Broadcasting zu einem Einzeiler anstatt zu einer Schleife über alle Verbindungen.

Broadcasting und Targeting

Socket.IO verwendet ein kompaktes Vokabular dafür, wer ein Event empfängt. Die richtige Wahl verhindert sowohl Datenlecks als auch unnötigen Fan-out.

  • io.emit(...) — jeder verbundene Socket. In der Regel nicht das, was man möchte.
  • socket.emit(...) — nur der Socket, der das aktuelle Event verarbeitet.
  • socket.broadcast.emit(...) — alle außer dem Absender.
  • socket.to(room).emit(...) — alle im Raum außer dem Absender.
  • io.to(room).emit(...) — alle im Raum, einschließlich des Absenders.
  • io.to(roomA).to(roomB).emit(...) — die Vereinigung beider Räume.
  • socket.to(socketId).emit(...) — ein spezifischer Socket anhand seiner ID.
socket.on("typing", ({ room }) => {
  // Tell the room, but not the person typing.
  socket.to(room).emit("typing", { user: socket.data.user.id });
});

Das Chainen von to vereint die Empfänger; ein Intersection-Operator ist in der Core-API nicht vorhanden. Wenn Sie „Mitglieder von Raum A, die gleichzeitig Admins sind“ benötigen, modellieren Sie dies als eigenen Raum — room:${id}:admins —, anstatt zu versuchen, dies zum Zeitpunkt des Emits zu berechnen.

Den Handshake authentifizieren

Die Authentifizierung gehört in den Connection-Handshake, nicht in eine erste Nachricht. Socket.IO middleware, die mit io.use registriert wurde, wird vor dem connection-Event ausgeführt. So können Sie einen nicht authentifizierten Socket ablehnen, bevor dieser irgendetwas senden oder einem Raum beitreten kann.

io.use((socket, next) => {
  const token = socket.handshake.auth.token;

  try {
    const payload = verifyToken(token);
    socket.data.user = { id: payload.sub, roles: payload.roles };
    next();
  } catch {
    next(new Error("unauthorized"));
  }
});

Zwei Details sind hierbei wichtig. Erstens ist socket.data der richtige Ort, um einen pro Verbindung spezifischen Status zu speichern; dieser bleibt über die gesamte Lebensdauer der Verbindung erhalten und ist in jedem Handler verfügbar. Zweitens löst eine abgelehnte Verbindung auf dem Client ein connect_error-Event aus, sodass die UI zur Eingabe eines neuen Tokens auffordern kann, anstatt endlos zu versuchen, die Verbindung wiederherzustellen.

Für Browser-Clients ist ein mit dem Handshake gesendetes Cookie eine Alternative zu einem expliziten Token, wodurch Sie bestehende Session-Infrastrukturen wiederverwenden können. In jedem Fall sollten Sie den cors-Origin auf Ihre eigene Anwendung setzen und jedes Event anhand des Benutzers in socket.data autorisieren — die Authentifizierung beweist, wer sich verbunden hat, nicht, was diese Person tun darf.

Wiederverbindung und Wiederherstellung des Verbindungszustands

Verbindungen brechen ab. Laptops gehen in den Ruhezustand, Smartphones wechseln das Netzwerk, Load Balancer werden neu gestartet. Socket.IO verbindet sich automatisch mit Exponential Backoff und Jitter neu und emittiert die Events reconnect_attempt und reconnect, damit Sie die entsprechende UI anzeigen können.

Standardmäßig gehen Events, die während der Abwesenheit des Clients gesendet wurden, jedoch verloren. Der Client verbindet sich als neuer Socket neu und setzt ab diesem Zeitpunkt fort. Das ist für einen Live-Feed akzeptabel, aber für einen Chat oder ein kollaboratives Dokument nicht ausreichend.

Die Connection state recovery löst das Problem bei kurzen Unterbrechungen. Aktivieren Sie diese auf dem Server, und ein wiederverbindender Client, der seine Session-ID übermittelt, erhält die verpassten Pakete – vorausgesetzt, die Trennung war kurzzeitig.

const io = new Server(httpServer, {
  connectionStateRecovery: {
    maxDisconnectionDuration: 2 * 60 * 1000,
    skipMiddlewares: true,
  },
});

Die Wiederherstellung ist bewusst begrenzt: Sie hält aktuelle Pakete für ein kurzes Zeitfenster im Speicher und nur auf der Instanz, die die ursprüngliche Verbindung hergestellt hat. Sie ist daher kein Ersatz für einen dauerhaften Speicher. Alles, was einen längeren Ausfall überstehen muss – Nachrichten, Bestellungen, Dokumentenänderungen –, gehört in eine Datenbank; der Socket dient dann nur dazu, die Clients über Änderungen zu benachrichtigen.

Skalierung mit dem Redis adapter

Ein Socket.IO server speichert seine Sockets und Rooms im Arbeitsspeicher. Wenn Sie zwei Instanzen hinter einem Load Balancer betreiben, verhalten sie sich effektiv wie zwei separate Echtzeit-Anwendungen: Eine Nachricht, die auf Instanz A gesendet wird, erreicht niemals Clients auf Instanz B.

Der Redis adapter löst dieses Problem. Jede Instanz veröffentlicht ihre Broadcasts in einem Redis pub/sub Channel und abonniert denselben Channel. So wird ein emit auf einer Instanz an alle passenden Sockets über alle Instanzen hinweg zugestellt.

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()]);

const io = new Server(httpServer, {
  adapter: createAdapter(pubClient, subClient),
});

Zwei betriebliche Hinweise: Erstens sind während der HTTP-Polling-Phase weiterhin sticky sessions erforderlich, da der Engine.IO Handshake über mehrere Requests erfolgt, die dieselbe Instanz erreichen müssen. Konfigurieren Sie den Load Balancer für eine cookie-basierte Affinity oder erzwingen Sie ausschließlich den WebSocket Transport. Zweitens nutzt der adapter Redis pub/sub, was schnell, aber nicht persistent ist: Eine Nachricht, die veröffentlicht wird, während eine Instanz neu startet, geht verloren. Redis wird zudem in einem eigenen Redis Guide behandelt.

Für sehr große Deployments gibt es einen sharded adapter, der Channels über einen Redis Cluster verteilt, sowie adapter für andere Broker, aber beginnen Sie mit dem Standard-adapter.

Events von außerhalb eines Sockets senden

Echtzeit-Events entstehen selten direkt innerhalb eines Connection-Handlers. Ein HTTP-Request erstellt einen Kommentar, ein Worker schließt einen Bericht ab, ein Webhook bestätigt eine Zahlung – all diese Ereignisse müssen verbundene Clients benachrichtigen.

Behalten Sie eine Referenz auf io und senden Sie von überall aus Events an einen Room:

app.post("/comments", async (req, res) => {
  const comment = await db.comment.create({ data: req.body });

  io.to(`post:${comment.postId}`).emit("comment:new", comment);

  res.status(201).json(comment);
});

Rooms sind hierfür das richtige Werkzeug, da sie über Instanzen hinweg geteilt werden, sofern der Adapter konfiguriert ist. Um einen spezifischen Benutzer zu erreichen, senden Sie das Event an einen benutzerbezogenen Room — user:${id} —, dem der Socket zum Zeitpunkt der Verbindung beigetreten ist, anstatt Socket-IDs zu tracken. Das funktioniert auch dann weiterhin, wenn der Benutzer zwei Tabs geöffnet hat, sich neu verbindet oder auf einer anderen Instanz landet.

Falls Sie doch eine Socket-ID benötigen, gibt io.in(room).fetchSockets() die aktiven Sockets in einem Room zurück, was nützlich für Presence-Zähler und gezielte Sendevorgänge ist.

Präsenz, Tipp-Indikatoren und kollaborative Features

Rooms in Kombination mit einem shared store decken die meisten kollaborativen Features ab. Die Präsenz – also wer gerade online ist – ist dabei das gängigste Feature, wobei die naive Umsetzung scheitert, sobald mehr als eine Instanz läuft.

Das bewährte Muster besteht darin, die maßgebliche Präsenz in Redis zu speichern statt in einer JavaScript map und die Daten bei einer Trennung (disconnect) zu bereinigen:

io.on("connection", async (socket) => {
  const { id } = socket.data.user;
  await redis.sadd("online", id);

  socket.on("disconnect", async () => {
    const sockets = await io.in(`user:${id}`).fetchSockets();
    if (sockets.length === 0) await redis.srem("online", id);
  });
});

Die fetchSockets Prüfung ist hierbei entscheidend: Ein Nutzer mit zwei offenen Tabs sollte nicht als offline angezeigt werden, wenn nur einer der Tabs geschlossen wird. Tipp-Indikatoren, Cursor-Positionen und “jemand bearbeitet gerade”-Flags folgen demselben Prinzip – es handelt sich um ephemeren State, der an einen Room broadcastet wird, mit einer kurzen Ablaufzeit, damit ein abgestürzter Client nicht dauerhaft einen veralteten Indikator hinterlässt.

Für echtes kollaboratives Editieren mit Konfliktlösung empfiehlt es sich, eine CRDT-Library wie Yjs zu verwenden und deren Updates über den Socket zu senden, anstatt einen eigenen Merge-Algorithmus zu entwickeln.

Validierung, Rate Limiting und Sicherheit

Ein Socket ist ein nicht vertrauenswürdiger Input-Kanal, genau wie ein HTTP-Request. Behandle jeden Event-Payload als potenziell gefährlich, bis er validiert wurde.

  • Shapes validieren. Parse Payloads mit einer Schema-Library wie Zod, bevor du sie verarbeitest, und lehne sie mit einem Acknowledgement ab, anstatt einen Fehler zu werfen.
  • Rate Limiting. Ein Socket kann tausende Events pro Sekunde senden. Nutze einen Token-Bucket pro Socket und trenne die Verbindung oder drossle Nutzer, die das Limit überschreiten.
  • Payload-Größe begrenzen. maxHttpBufferSize legt fest, wie viele Daten eine einzelne Nachricht maximal enthalten darf.
  • Jedes Event autorisieren. Überprüfe erneut, ob der Nutzer in socket.data berechtigt ist, in diesem Raum oder an dieser Ressource zu agieren, und verlasse dich nicht nur darauf, dass die Verbindung besteht.
  • Origin validieren. Setze cors.origin und lass diesen Wert in der Production-Umgebung nicht offen.
  • Timeouts implementieren. Heartbeats und Idle-Timeouts verhindern, dass verwaiste Verbindungen Ressourcen verbrauchen.
io.on("connection", (socket) => {
  socket.use(([event, payload], next) => {
    const parsed = MessageSchema.safeParse(payload);
    if (!parsed.success) return next(new Error("invalid_payload"));
    if (!takeToken(socket.id)) return next(new Error("rate_limited"));
    next();
  });
});

Per-Socket middleware, die mit socket.use registriert wird, ist der ideale Ort, um diese Prüfungen zu zentralisieren, damit die einzelnen Handler auf der Business-Logik fokussiert bleiben.

Debugging und Observability

Das erste Tool ist bereits integriert. Das Setzen von DEBUG=socket.io:* (oder engine*) gibt den Handshake, Transport-Upgrades und den Paketfluss aus, was in der Regel ausreicht, um einen Client zu diagnostizieren, der keine Verbindung herstellt.

Für die Produktion sollten Sie die Signale tracken, die tatsächlich auf Incidents hindeuten:

  • Verbundene Sockets — ein plötzlicher Abfall bedeutet ein Deployment, einen Crash oder eine Netzwerkpartitionierung.
  • Events pro Sekunde, nach Name — ein Spike bei einem bestimmten Event ist oft ein außer Kontrolle geratener Client-Loop.
  • Disconnect-Gründeping timeout und transport close deuten auf Netzwerk- oder Proxy-Probleme hin, während io server disconnect bedeutet, dass Ihr Code den Socket geschlossen hat.
  • Adapter-Health — wenn Redis pub/sub langsam ist oder die Verbindung unterbrochen wurde, stoppen Cross-Instance-Broadcasts, ohne dass ein Socket als fehlerhaft erscheint.
io.on("connection", (socket) => {
  socket.on("disconnect", (reason) => {
    metrics.increment("socket.disconnect", { reason });
  });
});

Das @socket.io/admin-ui Paket fügt ein Dashboard über dieselben Daten hinzu. Es lohnt sich, dieses in der Staging-Umgebung zu betreiben, damit Sie Rooms und Sockets in Echtzeit beobachten können. Wie bei jedem Echtzeitsystem sind die verwirrendsten Fehler die partiellen: Eine Instanz funktioniert einwandfrei, eine andere nicht, und nur eine Ansicht pro Instanz macht dies sichtbar.

Einen Real-time-Server testen

Real-time-Code ist testbar. Starten Sie den Server auf einem ephemeral Port, verbinden Sie einige socket.io-client-Instanzen und prüfen Sie die Ereignisse, die diese empfangen. Die wichtigste Disziplin besteht darin, auf Events zu warten (await), anstatt Pausen (sleep) einzulegen, damit die Tests schnell und deterministisch bleiben.

import { io as Client } from "socket.io-client";

test("broadcasts a message to the room", async () => {
  const a = Client(url, { auth: { token } });
  const b = Client(url, { auth: { token } });

  await Promise.all([once(a, "connect"), once(b, "connect")]);

  a.emit("room:join", "r1");
  b.emit("room:join", "r1");

  const received = once(b, "message:new");
  a.emit("message:send", { room: "r1", body: "hi" });

  const [message] = await received;
  expect(message.body).toBe("hi");

  a.close();
  b.close();
});

Testen Sie auch die Fehlerpfade, denn dort verstecken sich die Real-time-Bugs: Ein nicht authentifizierter Client sollte connect_error erhalten, ein fehlerhafter Payload sollte durch ein Acknowledgement abgelehnt werden und ein Socket, der die Verbindung trennt, sollte seine Rooms verlassen. Verwenden Sie pro Test einen eindeutigen Room oder Namespace, damit parallele Tests die Events der anderen nicht sehen, und schließen Sie die Clients immer, damit der Testprozess ordnungsgemäß beendet wird.

Einen Socket.IO-Server deployen

Ein Socket.IO-Deployment ist im Grunde ein HTTP-Deployment mit zwei zusätzlichen Anforderungen: langlebigen Verbindungen und einem gemeinsamen State. Die meisten Probleme entstehen, wenn man einen dieser Punkte vergisst.

  • Ein einziger Port. Binden Sie Socket.IO an denselben HTTP-Server wie Ihre API und terminieren Sie TLS am Proxy. Es gibt keinen separaten Port, den Sie freigeben müssen.
  • Proxy-Unterstützung für Upgrades. Nginx und die meisten Load Balancer benötigen eine explizite Konfiguration, um die Upgrade- und Connection-Header weiterzuleiten; ohne diese bleibt die Verbindung stillschweigend beim Polling.
  • Sticky Sessions. Cookie-basierte Affinity stellt sicher, dass der Polling-Handshake auf einer Instanz bleibt. Wenn Sie den Transport auf WebSocket-only erzwingen, ist Affinity weniger wichtig, aber der Handshake muss dennoch irgendwo abgeschlossen werden.
  • Redis adapter. Konfigurieren Sie diesen, bevor die zweite Instanz existiert – und nicht erst, wenn Benutzer über fehlende Nachrichten berichten.
  • Graceful Shutdown. Stoppen Sie bei SIGTERM die Annahme neuer Verbindungen und fahren Sie den Server kontrolliert herunter, damit laufende Events abgeschlossen werden können.
process.on("SIGTERM", async () => {
  io.close(); // disconnects clients and stops the server
  await pubClient.quit();
  await subClient.quit();
  httpServer.close();
});
upstream io_nodes {
  ip_hash;
  server 10.0.0.1:3000;
  server 10.0.0.2:3000;
}

Dimensionieren Sie jede Instanz basierend auf den Verbindungen, die sie hält, und nicht nur nach dem Request-Durchsatz. Jeder Socket verbraucht Speicher und einen File Descriptor. Eine einzelne Instanz mit zehntausenden Verbindungen wird auf eine Weise scheitern, wie es ein Request-basierter Service niemals tun würde. Skalieren Sie frühzeitig horizontal, behalten Sie die Verbindungszahlen im Auge und geben Sie Deployments eine ausreichend lange Grace Period, damit Clients sich mit einer gesunden Instanz neu verbinden können.

Wann reine WebSockets die bessere Wahl sind

Socket.IO ist nicht immer die richtige Lösung. Entscheiden Sie sich für das reine Protokoll, wenn:

  • Interoperabilität wichtig ist. Native WebSocket-Clients, andere Programmiersprachen und strikte Protocol-Tooling beherrschen den benutzerdefinierten Handshake von Socket.IO nicht.
  • Sie den kleinstmöglichen Client benötigen. Socket.IO liefert ein Client-Bundle mit; ein reiner WebSocket ist bereits im Browser integriert.
  • Ihre Infrastruktur WebSocket-native ist. Einige Gateways, Broker und Edge-Runtimes unterstützen WebSockets, aber nicht den Polling-Fallback von Socket.IO.
  • Sie beide Endpunkte kontrollieren und keine Abstraktion wünschen. Wenn Räume (Rooms) und die Wiederverbindung für Ihren Anwendungsfall trivial sind, ist ws einfacher zu durchschauen.

Wählen Sie im Gegenzug Socket.IO, wenn Sie Räume, Bestätigungen (Acknowledgements), automatische Wiederverbindungen und Multi-Instanzen-Broadcasting nutzen möchten, ohne diese selbst implementieren zu müssen. Für die meisten Produktteams deckt diese Liste den gesamten Funktionsumfang ihrer Echtzeit-Schicht ab – und genau deshalb existiert die Library.

Best Practices

  • Authentifizierung in io.use durchführen und den Benutzer in socket.data speichern, nicht in einem Closure.
  • Rooms nach echten Domain-Objekten modellieren – z. B. Conversations, Documents oder Tenants.
  • Acknowledgements für Events verwenden, deren Ergebnis der Client benötigt.
  • socket.to(room) bevorzugen, wenn der Absender sein eigenes Event nicht erhalten soll.
  • Connection State Recovery aktivieren, aber den dauerhaften Zustand in einer Datenbank speichern.
  • Den Redis Adapter hinzufügen, bevor eine zweite Instanz hinzugefügt wird, und Sticky Sessions konfigurieren.
  • In HTTP-Handlern und Workern über Rooms emitten, nicht über gespeicherte Socket-IDs.
  • Jedes Event validieren, autorisieren und mit Rate Limiting absichern.
  • Presence in disconnect bereinigen und fetchSockets verwenden, um mehrere Tabs zu handhaben.
  • maxHttpBufferSize, CORS-Origins und Timeouts explizit setzen.

Häufige Fehler

  • Aufruf von io.emit, wenn nur ein Raum das Event erhalten soll.
  • Die Annahme, dass Socket.IO und WebSockets austauschbar seien, was dann beim Verbindungsaufbau mit einem nativen Client scheitert.
  • Vertrauen auf socket.handshake.auth, ohne den Token zu verifizieren.
  • Speicherung der Presence in einem lokalen Map, wodurch diese hinter einem Load Balancer verloren geht.
  • Vergessen von Sticky Sessions, was den Polling-Handshake unterbricht.
  • Die Erwartung, dass bei einer Wiederverbindung Events automatisch erneut abgespielt werden, ohne eine Recovery des Verbindungszustands.
  • Verwendung von socket.id als User-ID, was bei einer Wiederverbindung zu Fehlern führt.
  • Durchführung von rechenintensiven oder blockierenden Aufgaben innerhalb eines Event-Handlers, wodurch jeder Socket auf der Instanz blockiert wird.
  • Überlassung der Payload-Validierung und des Rate Limiting an das Frontend.
  • Behandlung des Sockets als dauerhafter Speicher für Daten, die nicht verloren gehen dürfen.

Wie geht es weiter?

Socket.IO ist die High-Level-Schicht über dem Protokoll, das im Guide zu WebSockets behandelt wird. Dort werden Frames, Heartbeats und der Upgrade-Handshake erklärt, die du nun abstrahierst. Der Redis-Guide geht tiefer auf Pub/Sub und den Shared State hinter dem Adapter ein, und Node.js Basics erklärt den Event Loop, auf dem jeder Handler läuft. Wenn dein Socket.IO-Server an eine HTTP API angebunden ist, behandelt der Express-Guide das Routing und die middleware, mit denen er sich einen Prozess teilt.

In der Praxis

Server, Client, Rooms und Skalierung

Die vier Dateien hinter den meisten Echtzeit-Features.

server.ts
import { Server } from "socket.io";
import { createServer } from "node:http";

const httpServer = createServer(app);

const io = new Server(httpServer, {
  cors: {
    origin: process.env.APP_ORIGIN,
    credentials: true,
  },
});

io.use((socket, next) => {
  const token = socket.handshake.auth.token;
  try {
    socket.data.user = verifyToken(token);
    next();
  } catch {
    next(new Error("unauthorized"));
  }
});

io.on("connection", (socket) => {
  socket.join(`user:${socket.data.user.id}`);
  socket.emit("ready", { id: socket.id });
});

httpServer.listen(3000);

Socket.IO Rooms vs. Raw WebSocket Channels

Socket.IO übernimmt die Mitgliederverwaltung für Sie. Beim Raw-Protokoll müssen Sie eine Map der Verbindungen selbst pflegen und die Verteilung manuell steuern, was leicht zu subtilen Fehlern führen kann.

Socket.IO
socket.join(`org:${orgId}`);
io.to(`org:${orgId}`).emit("update", payload);
Raw WebSocket
// You own membership, fan-out and cleanup.
const rooms = new Map<string, Set<WebSocket>>();
for (const ws of rooms.get(`org:${orgId}`) ?? []) {
  if (ws.readyState === ws.OPEN) ws.send(payload);
}

Acknowledgement vs. Fire-and-Forget Emit

Ein Callback macht aus einem Emit einen Request/Response-Zyklus und macht Fehler sichtbar. Fire-and-Forget ist für reine Broadcasts in Ordnung, aber nicht für Aktionen, die abgelehnt werden können.

Mit Ack
socket.emit("order:create", order, (result) => {
  if (!result.ok) showError(result.error);
  else markCreated(result.id);
});
Fire and Forget
socket.emit("order:create", order);
// No idea whether the server accepted,
// rejected or crashed.

Abwägungen

Ist Socket.IO die richtige Abstraktion?

Socket.IO tauscht eine kleine Protokollschicht und einen erforderlichen Client gegen eine Menge an Echtzeit-Infrastruktur ein, die Sie sonst selbst schreiben müssten.

Strengths

  • Echtzeit-Features statt Infrastruktur

    Rooms, Acknowledgements, Reconnection, Heartbeats und ein Polling-Fallback sind enthalten. Das erspart Ihnen Wochen an Edge-Cases, die Sie sonst erst in der Produktion entdecken würden.

  • Einheitliche API auf beiden Seiten

    Server und Client teilen sich dasselbe Event-Modell, sodass ein Feature meist nur aus wenigen Zeilen auf jeder Seite besteht. Das Onboarding eines Frontend-Entwicklers dauert nur Minuten.

  • Skalierbar über Instanzen

    Der Redis adapter verwandelt eine Flotte von Pods in einen einzigen logischen Server für Rooms und Broadcasts – der schwierigste Teil jedes Echtzeit-Deployments.

Trade-offs

  • Es ist kein reiner WebSocket

    Socket.IO definiert ein eigenes Protokoll auf Basis von Engine.IO. Ein nativer WebSocket-Client kann sich nicht verbinden, daher benötigen Sie den Socket.IO-Client auf jeder Plattform.

  • Rooms leben im Speicher

    Die Room-Mitgliedschaft wird pro Prozess gespeichert, außer Sie verwenden einen Adapter. Ohne Redis kann ein Reconnect auf einer anderen Instanz stillschweigend am falschen Ort landen.

  • Gefahr von Over-Broadcasting

    io.emit sendet an jeden verbundenen Socket. Es ist nur ein Tastendruck entfernt und kann Daten leaken oder ein großes Deployment überlasten; adressieren Sie Rooms daher gezielt.

Häufig gestellte Fragen

Häufig gestellte Fragen

Keep learning

Related topics from the roadmap.

$ Lernen Sie jetzt

Bereit, Socket.IO zu lernen?

Unser interaktives Tutorial führt Sie Schritt für Schritt durch Socket.IO — mit Quizzen und echtem Code, den Sie im Browser ausführen können.