← Zurück zum Blog

Nitro v3 WebSockets - wann sie wirklich Sinn machen (und wann nicht)

Ein typischer Projektmoment:

„Können wir das in Echtzeit anzeigen?"

Die Antwort war in Nuxt bisher:

„Ja… aber je nach Hosting-Plattform wird das kompliziert."

Weil:

  • Edge Runtimes haben Limits
  • Serverless Environments unterstützen WebSockets oft nicht richtig
  • Node.js vs. Edge = komplett unterschiedliche APIs
  • Deployment wird zur Hölle

Mit Nitro v3 ändert sich das.

Nitro bringt ein vereinheitlichtes WebSocket API, das über verschiedene Runtimes funktionieren soll.

Das klingt erstmal fantastisch.

Aber:

Nicht jede Plattform kann WebSockets gleich gut.

In diesem Artikel schauen wir uns an:

  • Wie das WebSocket API in Nitro v3 funktioniert
  • Wann du WebSockets wirklich brauchst
  • Warum Edge + WebSockets oft nicht passt
  • Alternativen (SSE, Polling, Long-Polling)
  • Praktische Use Cases

Das Problem: Real-Time in Nuxt war „Plattform-abhängig"

Wenn man ehrlich ist:

Real-Time Features in Nuxt 3 waren möglich, aber oft frustrierend.

Die Situation in Nuxt 3

Je nach Deployment-Ziel hattest du völlig unterschiedliche Möglichkeiten:

PlattformWebSocketsWorkaround
Node.js✅ Ja (ws, socket.io)-
Vercel❌ NeinSSE oder Pusher
Netlify❌ NeinSSE oder Ably
Cloudflare Workers⚠️ EingeschränktDurable Objects
AWS Lambda❌ NeinAPI Gateway WebSocket

Das führte dazu, dass viele Projekte:

  • entweder auf Real-Time verzichtet haben
  • oder externe Services nutzen mussten (Pusher, Ably, Firebase)
  • oder unterschiedliche Implementierungen für unterschiedliche Environments geschrieben haben

Das war nicht schön.

Was Nitro v3 verspricht

Nitro v3 bringt ein plattform-übergreifendes WebSocket API.

Das bedeutet:

  • Ein API
  • funktioniert (theoretisch) auf Node, Edge, Serverless
  • keine unterschiedlichen Implementierungen mehr

Klingt perfekt.

Aber:

Die Plattform-Limits bleiben trotzdem.

Das ist wichtig zu verstehen.

Wie das WebSocket API in Nitro v3 funktioniert

Jetzt wird es konkret.

Hier ist, wie du WebSockets in Nitro v3 nutzt:

Server-Side: WebSocket Route definieren

// server/api/ws.ts
export default defineWebSocketHandler({
  open(peer) {
    console.log("[ws] New connection:", peer.id);
    peer.send({ type: "welcome", message: "Connected!" });
  },

  message(peer, message) {
    console.log("[ws] Message from", peer.id, ":", message);

    // Echo back
    peer.send({
      type: "echo",
      data: message,
    });

    // Broadcast to all
    peer.publish("chat", {
      type: "message",
      from: peer.id,
      text: message.text,
    });
  },

  close(peer, event) {
    console.log("[ws] Connection closed:", peer.id);
  },

  error(peer, error) {
    console.error("[ws] Error:", error);
  },
});

Das ist das Basis-Pattern.

Client-Side: WebSocket verbinden

Im Frontend (Vue Component):

<script setup>
import { ref, onMounted, onUnmounted } from "vue";

const messages = ref([]);
const ws = ref(null);

onMounted(() => {
  // Connect to WebSocket
  ws.value = new WebSocket("ws://localhost:3000/api/ws");

  ws.value.onopen = () => {
    console.log("Connected");
  };

  ws.value.onmessage = (event) => {
    const data = JSON.parse(event.data);
    messages.value.push(data);
  };

  ws.value.onclose = () => {
    console.log("Disconnected");
  };
});

onUnmounted(() => {
  ws.value?.close();
});

function sendMessage(text) {
  ws.value?.send(JSON.stringify({ text }));
}
</script>

<template>
  <div>
    <div v-for="msg in messages" :key="msg.id">
      {{ msg.text }}
    </div>
    <button @click="sendMessage('Hello')">Send</button>
  </div>
</template>

Broadcasting: Nachricht an alle senden

// server/utils/websocket.ts
export function broadcastToAll(event: string, data: any) {
  // Get all connected peers
  const peers = useWebSocketPeers();

  // Send to everyone
  peers.forEach((peer) => {
    peer.send({ event, data });
  });
}

Dann von überall nutzen:

// server/api/trigger-update.ts
export default defineEventHandler(async (event) => {
  // Do something (e.g., update database)
  await updateSomething();

  // Notify all connected clients
  broadcastToAll("data-updated", {
    timestamp: Date.now(),
  });

  return { success: true };
});

Rooms / Channels

Für gezielte Broadcasts:

// server/api/ws.ts
export default defineWebSocketHandler({
  open(peer) {
    // Subscribe to channel
    const userId = peer.request.headers.get("user-id");
    peer.subscribe(`user:${userId}`);
  },

  message(peer, message) {
    if (message.type === "join-room") {
      peer.subscribe(`room:${message.roomId}`);
    }

    if (message.type === "send-to-room") {
      peer.publish(`room:${message.roomId}`, {
        from: peer.id,
        text: message.text,
      });
    }
  },
});

Das ist die Theorie.

Jetzt kommt der Reality-Check.

Plattform-Limits: Warum Edge + WebSockets oft nicht passt

Hier kommt der Teil, den viele Tutorials vergessen.

WebSockets brauchen „long-lived connections".

Das bedeutet:

  • Connection bleibt offen (Minuten, Stunden)
  • Server muss Connection halten
  • bidirektionale Kommunikation

Aber:

Viele moderne Plattformen sind für kurze Requests optimiert.

Vercel: WebSockets = ❌

Vercel unterstützt keine echten WebSockets auf Serverless Functions.

Warum?

  • Functions haben Timeout (10-300 Sekunden je nach Plan)
  • Nach Timeout = Connection abbgebrochen
  • Nicht für long-lived connections designed

Offizieller Workaround:

  • Pusher
  • Ably
  • oder SSE (Server-Sent Events)

Netlify: Ähnliches Problem

Netlify Functions haben ähnliche Limits:

  • Background Functions: max 15 Minuten
  • Edge Functions: sehr kurze Timeouts

Workaround:

  • Externe Services
  • oder SSE

Cloudflare Workers: Es geht - aber anders

Cloudflare Workers unterstützen WebSockets - aber nur mit Durable Objects.

Das bedeutet:

  • extra Setup
  • extra Kosten
  • extra Komplexität

Es geht, aber es ist nicht „einfach WebSocket Route erstellen".

AWS Lambda: Nein (außer mit API Gateway WebSocket)

Lambda Functions unterstützen keine direkten WebSockets.

Du brauchst:

  • API Gateway WebSocket
  • extra Setup
  • extra Routing-Logik

Node.js / VPS / Container: ✅ Ja

Wenn du auf:

  • klassischem Node.js Server
  • VPS (Hetzner, DigitalOcean, etc.)
  • Docker Container
  • Kubernetes

deployest, dann funktionieren WebSockets problemlos.

Zusammenfassung: Deployment vs. WebSocket Support

DeploymentWebSocket SupportEmpfehlung
Node.js Server✅ VollWebSockets nutzen
VPS / Container✅ VollWebSockets nutzen
Vercel❌ NeinSSE oder externe Services
Netlify❌ NeinSSE oder externe Services
Cloudflare Workers⚠️ Mit Durable ObjectsNur wenn nötig
AWS Lambda⚠️ Mit API GatewayKomplexes Setup

Die wichtigste Erkenntnis:

Nitro v3 vereinheitlicht das API - aber es kann die Plattform-Limits nicht aufheben.

Wann du WebSockets wirklich brauchst

Jetzt kommt die wichtigste Frage:

Brauchst du überhaupt WebSockets?

Viele Projekte denken:

„Wir wollen Real-Time" → „Also brauchen wir WebSockets"

Das ist nicht immer richtig.

Use Cases, wo WebSockets Sinn machen

✅ Chat / Messaging

  • bidirektional
  • instant messages
  • typing indicators
// server/api/chat-ws.ts
export default defineWebSocketHandler({
  message(peer, message) {
    if (message.type === "typing") {
      peer.publish("chat-room", {
        type: "user-typing",
        userId: peer.id,
      });
    }

    if (message.type === "message") {
      peer.publish("chat-room", {
        type: "new-message",
        from: peer.id,
        text: message.text,
        timestamp: Date.now(),
      });
    }
  },
});

✅ Live Collaboration (Google Docs Style)

  • mehrere User editieren gleichzeitig
  • Cursor-Position
  • Änderungen in Echtzeit

✅ Live Monitoring / Dashboards

  • Server Metrics
  • Deployment Status
  • Error Tracking
// server/api/monitoring-ws.ts
export default defineWebSocketHandler({
  open(peer) {
    // Send initial state
    peer.send({
      type: "initial",
      data: getCurrentMetrics(),
    });

    // Subscribe to updates
    peer.subscribe("metrics-updates");
  },
});

// Somewhere else: push updates
export function pushMetricsUpdate(metrics: any) {
  broadcastToChannel("metrics-updates", {
    type: "metrics",
    data: metrics,
  });
}

✅ Multiplayer Games

  • Player-Position
  • Game State
  • Input-Events

✅ Live Auctions / Trading

  • Bid Updates
  • Price Changes
  • instant feedback

Use Cases, wo WebSockets NICHT nötig sind

❌ Notifications („Sie haben eine neue Nachricht")

Hier reicht oft:

  • Polling (alle 30 Sekunden)
  • oder SSE (Server-Sent Events)

❌ Dashboard Updates (alle 10 Sekunden)

Hier reicht:

  • setInterval + API Call
// composables/useRealtimeData.ts
export function useRealtimeData() {
  const data = ref(null);

  async function refresh() {
    data.value = await $fetch("/api/data");
  }

  onMounted(() => {
    refresh();
    const interval = setInterval(refresh, 10000); // every 10s

    onUnmounted(() => clearInterval(interval));
  });

  return { data };
}

❌ „Live" Content Updates (Newsfeed, Blog)

Hier reicht:

  • Polling
  • oder gar nichts (User refreshed eh manuell)

❌ Form Validation

WebSockets für Form Validation ist Overkill.

Hier reicht:

  • normale API Calls

Alternative: Server-Sent Events (SSE)

Wenn du unidirektionale Updates brauchst (Server → Client), ist SSE oft besser als WebSockets.

Warum SSE manchmal besser ist

FeatureWebSocketsSSE
BidirektionalJaNein (nur Server → Client)
ProtokollWebSocketHTTP
Reconnectmanuellautomatisch
Vercel Support⚠️ Eingeschränkt
Browser Support
KomplexitätHöherNiedriger

SSE ist perfekt für:

  • Notifications
  • Live Updates
  • Dashboard Metrics
  • Progress Updates

SSE in Nitro v3

// server/api/sse.ts
export default defineEventHandler(async (event) => {
  const stream = createEventStream(event);

  // Send initial data
  await stream.push({
    type: "welcome",
    data: "Connected to SSE",
  });

  // Send updates every 5 seconds
  const interval = setInterval(async () => {
    await stream.push({
      type: "update",
      data: {
        timestamp: Date.now(),
        metrics: getCurrentMetrics(),
      },
    });
  }, 5000);

  // Cleanup on close
  stream.onClosed(async () => {
    clearInterval(interval);
    await stream.close();
  });

  return stream.send();
});

Client:

// composables/useSSE.ts
export function useSSE(url: string) {
  const data = ref(null);
  const error = ref(null);

  onMounted(() => {
    const eventSource = new EventSource(url);

    eventSource.onmessage = (event) => {
      data.value = JSON.parse(event.data);
    };

    eventSource.onerror = (err) => {
      error.value = err;
    };

    onUnmounted(() => {
      eventSource.close();
    });
  });

  return { data, error };
}

Vorteil:

  • einfacher als WebSockets
  • funktioniert auf mehr Plattformen
  • automatisches Reconnect

Nachteil:

  • nur Server → Client (nicht bidirektional)

Praktische Use Cases (aus echten Projekten)

1) Live Task Progress

// server/tasks/long-import.ts
export default defineTask({
  meta: { name: "long-import" },
  run: async ({ payload }) => {
    const { userId, fileUrl } = payload;

    const records = await downloadAndParse(fileUrl);
    const total = records.length;

    for (let i = 0; i < records.length; i++) {
      await importRecord(records[i]);

      // Send progress update
      if (i % 10 === 0) {
        broadcastToUser(userId, {
          type: "import-progress",
          progress: Math.round((i / total) * 100),
          current: i,
          total,
        });
      }
    }

    broadcastToUser(userId, {
      type: "import-complete",
      total,
    });

    return { imported: total };
  },
});

Client:

<script setup>
const progress = ref(0);

const ws = new WebSocket("/api/ws");

ws.onmessage = (event) => {
  const data = JSON.parse(event.data);
  if (data.type === "import-progress") {
    progress.value = data.progress;
  }
};

function startImport() {
  $fetch("/api/start-import", { method: "POST" });
}
</script>

<template>
  <div>
    <button @click="startImport">Start Import</button>
    <div v-if="progress > 0">Progress: {{ progress }}%</div>
  </div>
</template>

2) Live Notifications

// server/utils/notifications.ts
export function sendNotification(userId: string, notification: any) {
  // Send via WebSocket
  broadcastToUser(userId, {
    type: "notification",
    data: notification,
  });

  // Also store in DB
  db.notifications.create({
    data: {
      userId,
      ...notification,
    },
  });
}

3) Live Dashboard (Business Central Integration)

// server/tasks/sync-bc-metrics.ts
export default defineTask({
  meta: { name: "sync-bc-metrics" },
  schedule: "*/5 * * * *", // every 5 minutes
  run: async () => {
    const bcClient = await getBusinessCentralClient();

    const metrics = {
      sales: await bcClient.getSalesMetrics(),
      inventory: await bcClient.getInventoryMetrics(),
      orders: await bcClient.getOrdersMetrics(),
    };

    // Broadcast to all connected dashboards
    broadcastToAll({
      type: "metrics-update",
      data: metrics,
      timestamp: Date.now(),
    });

    return { success: true };
  },
});

💡 Seitennotiz:
Wenn du OAuth für Business Central nutzt, schau dir OAuth in Business Central an.

Best Practices

✅ Heartbeat / Ping-Pong

Viele Plattformen schließen idle connections.

Lösung: Heartbeat

// server/api/ws.ts
const clients = new Map();

export default defineWebSocketHandler({
  open(peer) {
    // Start heartbeat
    const interval = setInterval(() => {
      peer.send({ type: "ping" });
    }, 30000); // every 30s

    clients.set(peer.id, { peer, interval });
  },

  close(peer) {
    const client = clients.get(peer.id);
    if (client) {
      clearInterval(client.interval);
      clients.delete(peer.id);
    }
  },
});

Client:

ws.onmessage = (event) => {
  const data = JSON.parse(event.data);
  if (data.type === "ping") {
    ws.send(JSON.stringify({ type: "pong" }));
  }
};

✅ Reconnect Logic

Connections können abbrechen.

Client-Side Reconnect:

function connectWebSocket() {
  const ws = new WebSocket("/api/ws");

  ws.onclose = () => {
    console.log("Disconnected. Reconnecting in 3s...");
    setTimeout(connectWebSocket, 3000);
  };

  return ws;
}

✅ Authentication

WebSockets sollten authentifiziert sein:

// server/api/ws.ts
export default defineWebSocketHandler({
  async open(peer) {
    // Check auth token
    const token = peer.request.headers.get("authorization");

    if (!token) {
      peer.close(1008, "Unauthorized");
      return;
    }

    const user = await verifyToken(token);
    if (!user) {
      peer.close(1008, "Invalid token");
      return;
    }

    // Store user info
    peer.context.user = user;
    peer.subscribe(`user:${user.id}`);
  },
});

✅ Rate Limiting

Prevent abuse:

const rateLimits = new Map();

export default defineWebSocketHandler({
  message(peer, message) {
    const limit = rateLimits.get(peer.id) || { count: 0, reset: Date.now() };

    if (Date.now() > limit.reset) {
      limit.count = 0;
      limit.reset = Date.now() + 60000; // 1 minute
    }

    limit.count++;
    rateLimits.set(peer.id, limit);

    if (limit.count > 100) {
      peer.send({ type: "error", message: "Rate limit exceeded" });
      return;
    }

    // Handle message...
  },
});

Fazit

Nitro v3 bringt ein vereinheitlichtes WebSocket API.

Das ist gut.

Aber:

Plattform-Limits bleiben bestehen.

Die wichtigsten Erkenntnisse:

  1. WebSockets funktionieren am besten auf Node.js / VPS / Container
  2. Vercel / Netlify = keine echten WebSockets (nutze SSE oder externe Services)
  3. WebSockets sind nicht immer nötig - oft reicht Polling oder SSE
  4. SSE ist oft die bessere Wahl für unidirektionale Updates

Wann WebSockets wirklich Sinn machen:

  • Chat / Messaging
  • Live Collaboration
  • Multiplayer Games
  • Live Auctions

Wann WebSockets Overkill sind:

  • Notifications
  • Dashboard Updates (alle X Sekunden)
  • Form Validation

Fazit:

Nutze WebSockets, wenn du sie wirklich brauchst.
Nicht, weil es „cool" klingt.

Und:

Prüfe dein Deployment-Ziel, bevor du WebSockets einbaust.


Die Artikel-Serie:

  1. Nuxt 5 kommt
  2. Nitro v3 Tasks & Background Jobs
  3. Nitro v3 WebSockets (dieser Artikel)
  4. Nitro v3 Breaking Changes
  5. Migration Guide: Nuxt 5

Wenn du Real-Time Features planst: Wähle die Technologie basierend auf deinem Deployment-Ziel, nicht basierend auf Hype.