· WebSocket· polling· SSE· IoT· realtid· FastAPI· React· TanStack Query· dashboard· performance
WebSocket vs. polling til IoT dashboard — hvornår bruge hvad?
WebSocket vs. HTTP polling til realtids IoT dashboards: latens, skalering, ressourceforbrug, Server-Sent Events, FastAPI WebSocket og TanStack Query polling sammenligning.
Af M-Bus Gateway
IoT dashboards kræver realtids-data. Her er hvornår WebSocket, Server-Sent Events (SSE) og HTTP-polling er de rigtige valg.
Sammenligning: Latens og ressourceforbrug
HTTP Polling (setInterval):
Latens: Interval/2 i gennemsnit (10s interval = 5s gennemsnit)
Ressourcer: Ny HTTP-forbindelse pr. poll → overhead
Skalering: Lineært: 1.000 klienter × 10 polls/min = 10.000 req/min
Egnet til: Data der opdateres sjældnere end polling-interval
Uegnet til: < 1 sekundslatens, live streaming
Server-Sent Events (SSE):
Latens: < 100ms (server pusher øjeblikkeligt)
Ressourcer: Persistent HTTP-forbindelse (keep-alive)
Skalering: 1 forbindelse pr. klient — men server holder state
Egnet til: Uni-direktionel server → klient (alarmer, status)
Uegnet til: Bi-direktionel kommunikation
WebSocket (ws://):
Latens: < 10ms (full-duplex)
Ressourcer: Persistent TCP-forbindelse
Skalering: 1 socket pr. klient — RAM per connection
Egnet til: Chat, live kommandoer, bi-direktionel IoT
Uegnet til: Simple read-only dashboards (overkill)
M-Bus Gateway: Hvilken metode?
Gateway-data: 1 aflæsning pr. dag (kl. 06:00 UTC)
→ Intet behov for WebSocket — data skifter ikke i løbet af dagen
Undtagelser der kræver realtids:
→ Gateway-alarm (batteridød, signaltab, sabotage)
→ Gateway online/offline status
→ MQTT-kommandostatus (OTA, remote reset)
Anbefalet arkitektur:
→ Normal dashboard: TanStack Query polling (30 sek)
→ Alarmer: SSE stream fra /api/v1/alarms/stream
→ Gateway status: SSE stream eller 30 sek polling
→ OTA progress: WebSocket (bi-direktionel status)
FastAPI SSE: Alarmstream
# server/src/alarms/router.py
from fastapi import APIRouter
from fastapi.responses import StreamingResponse
import asyncio
import json
router = APIRouter()
@router.get("/stream")
async def alarm_stream(
user: TokenPayload = Depends(get_current_user),
):
"""
Server-Sent Events stream til alarmer.
Klienten opretter EventSource — server pusher ved nye alarmer.
"""
async def event_generator():
# Initial: Send åbne alarmer:
alarms = await get_open_alarms(user.tenant_id)
yield f"data: {json.dumps({'type': 'initial', 'alarms': alarms})}\n\n"
# Løbende: Lyt på Redis pub/sub kanal:
pubsub = redis_client.pubsub()
await pubsub.subscribe(f"alarms:{user.tenant_id}")
try:
while True:
message = await pubsub.get_message(ignore_subscribe_messages=True)
if message:
yield f"data: {message['data'].decode()}\n\n"
else:
# Heartbeat hvert 30 sek (forhindrer proxy timeout):
yield ": heartbeat\n\n"
await asyncio.sleep(30)
except asyncio.CancelledError:
await pubsub.unsubscribe()
return StreamingResponse(
event_generator(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"X-Accel-Buffering": "no", # Nginx: Disable buffering for SSE
},
)
React SSE klient
// ui/src/hooks/useAlarmStream.ts
import { useState, useEffect } from "react";
import { useAuthStore } from "@/stores/auth";
import type { Alarm } from "@/api";
export function useAlarmStream() {
const [alarms, setAlarms] = useState<Alarm[]>([]);
const [connected, setConnected] = useState(false);
const token = useAuthStore((s) => s.token);
useEffect(() => {
if (!token) return;
const url = `/api/v1/alarms/stream?token=${token}`;
const eventSource = new EventSource(url);
eventSource.onopen = () => setConnected(true);
eventSource.onmessage = (e) => {
const data = JSON.parse(e.data);
if (data.type === "initial") {
setAlarms(data.alarms);
} else if (data.type === "new_alarm") {
setAlarms((prev) => [data.alarm, ...prev]);
} else if (data.type === "alarm_resolved") {
setAlarms((prev) => prev.filter((a) => a.id !== data.alarm_id));
}
};
eventSource.onerror = () => {
setConnected(false);
// Browser auto-reconnects efter 3 sek
};
return () => {
eventSource.close();
setConnected(false);
};
}, [token]);
return { alarms, connected };
}
FastAPI WebSocket: OTA progress
# server/src/gateway/router.py
from fastapi import WebSocket, WebSocketDisconnect
@router.websocket("/{gateway_id}/ota-progress")
async def ota_progress_ws(
gateway_id: str,
websocket: WebSocket,
token: str, # JWT som query param (WebSocket kan ikke sende headers)
):
"""
WebSocket til real-time OTA-progress fra gateway.
Gateway sender procenttal via MQTT → server forwarder via WS.
"""
# Validér token:
try:
user = verify_token(token)
except JWTError:
await websocket.close(code=4001)
return
await websocket.accept()
# Subscribe på MQTT-topic for dette gateway:
ota_topic = f"meters/{gateway_id}/ota-progress"
async def mqtt_callback(msg: dict):
try:
await websocket.send_json(msg)
except Exception:
pass
try:
await subscribe_mqtt(ota_topic, mqtt_callback)
# Hold forbindelsen åben:
while True:
await asyncio.sleep(1)
except WebSocketDisconnect:
pass
finally:
await unsubscribe_mqtt(ota_topic, mqtt_callback)
TanStack Query polling: Enklere alternativ
// For de fleste IoT dashboard use-cases er polling tilstrækkeligt:
function useGatewayFleetStatus() {
return useQuery({
queryKey: ["gateways", "fleet-status"],
queryFn: () => portalGateways.getFleetStatus(),
// Poll hvert 30 sekunder (gateway sender heartbeat hvert 5 min):
refetchInterval: 30 * 1000,
// Stop polling ved tab-skift:
refetchIntervalInBackground: false,
// Vis stale data øjeblikkeligt, fetch i baggrunden:
staleTime: 0,
});
}
// Verdict: Brug polling til dashboard-data (30-60 sek interval)
// Brug SSE til alarmer (push-baseret, ingen polling)
// Brug WebSocket kun til bi-direktionel real-time (OTA, kommandoer)
Konklusion
For et IoT-dashboard med daglige M-Bus aflæsninger er HTTP-polling (30 sek) via TanStack Query den korrekte løsning — simpelt, fejlresistent og skalerbart. Server-Sent Events egner sig til alarmer og statusopdateringer (server pusher øjeblikkeligt). WebSocket er kun relevant ved bi-direktionel realtidskommunikation som OTA-progress og live MQTT-kommandoer. Undgå WebSocket til simpel read-only data — det er kompleksitet uden fordel.