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:
| Plattform | WebSockets | Workaround |
|---|---|---|
| Node.js | ✅ Ja (ws, socket.io) | - |
| Vercel | ❌ Nein | SSE oder Pusher |
| Netlify | ❌ Nein | SSE oder Ably |
| Cloudflare Workers | ⚠️ Eingeschränkt | Durable Objects |
| AWS Lambda | ❌ Nein | API 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
| Deployment | WebSocket Support | Empfehlung |
|---|---|---|
| Node.js Server | ✅ Voll | WebSockets nutzen |
| VPS / Container | ✅ Voll | WebSockets nutzen |
| Vercel | ❌ Nein | SSE oder externe Services |
| Netlify | ❌ Nein | SSE oder externe Services |
| Cloudflare Workers | ⚠️ Mit Durable Objects | Nur wenn nötig |
| AWS Lambda | ⚠️ Mit API Gateway | Komplexes 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
| Feature | WebSockets | SSE |
|---|---|---|
| Bidirektional | Ja | Nein (nur Server → Client) |
| Protokoll | WebSocket | HTTP |
| Reconnect | manuell | automatisch |
| Vercel Support | ❌ | ⚠️ Eingeschränkt |
| Browser Support | ✅ | ✅ |
| Komplexität | Höher | Niedriger |
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:
- WebSockets funktionieren am besten auf Node.js / VPS / Container
- Vercel / Netlify = keine echten WebSockets (nutze SSE oder externe Services)
- WebSockets sind nicht immer nötig - oft reicht Polling oder SSE
- 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:
- Nuxt 5 kommt
- Nitro v3 Tasks & Background Jobs
- Nitro v3 WebSockets (dieser Artikel)
- Nitro v3 Breaking Changes
- Migration Guide: Nuxt 5
Wenn du Real-Time Features planst: Wähle die Technologie basierend auf deinem Deployment-Ziel, nicht basierend auf Hype.