Un dashboard IoT en tiempo real no es simplemente una página web con gráficas. Es un sistema completo que involucra ingestión de datos, procesamiento, almacenamiento, transmisión y renderizado, todo optimizado para que los datos lleguen del sensor al ojo del usuario en menos de un segundo.
En esta guía te explico cómo construir uno desde cero, basándome en la experiencia real de haberlos construido para clientes como Spherag (monitorización agrícola 24/7), InfoAdex (55M+ registros publicitarios) y Orquest (gestión de fuerza laboral).
Requisitos de un dashboard IoT real
Antes de elegir tecnologías, define qué necesita tu dashboard:
Funcionales
- Mostrar datos actualizados en tiempo real (< 1 segundo de latencia)
- Visualizar series temporales con zoom y navegación
- Alertas visuales cuando un valor sale de rango
- Múltiples fuentes de datos en una vista unificada
- Filtros por dispositivo, ubicación, rango temporal
- Exportación de datos (CSV, PDF)
No funcionales
- Funcionar 24/7 sin degradación de rendimiento
- Soportar cientos de widgets actualizándose simultáneamente
- Renderizar miles de puntos de datos sin bloquear el UI
- Reconexión automática si se pierde la conexión
- Responsive: funcionar en pantallas de control y móviles
- Tiempo de carga inicial < 3 segundos
Stack técnico recomendado
Después de iterar en múltiples proyectos, este es el stack que recomendamos para un dashboard IoT en tiempo real:
Frontend
| Tecnología | Para qué |
|---|---|
| React 18+ | Framework UI con Concurrent Features |
| Recharts o Visx | Gráficas de series temporales performantes |
| D3.js | Visualizaciones custom complejas |
| TanStack Query | Gestión de estado servidor + cache |
| Socket.IO o native WebSocket | Datos en tiempo real |
| Tailwind CSS | Estilos rápidos y consistentes |
| Zustand | Estado global ligero |
Backend
| Tecnología | Para qué |
|---|---|
| Node.js + Fastify | API REST + WebSocket server |
| Redis | Cache + pub/sub para broadcast |
| TimescaleDB | Base de datos de series temporales |
| PostgreSQL | Metadatos, configuraciones, usuarios |
| MQTT broker (EMQX/Mosquitto) | Recepción de datos de dispositivos |
Infraestructura
| Tecnología | Para qué |
|---|---|
| Docker + Docker Compose | Desarrollo local y despliegue |
| Kubernetes | Producción escalable |
| Nginx | Reverse proxy + WebSocket upgrade |
| Prometheus + Grafana | Monitorización del propio dashboard |
Arquitectura de datos
Flujo de datos end-to-end
Dispositivo IoT
↓ (MQTT)
Broker MQTT
↓ (suscripción)
Servicio de ingestión (Node.js)
↓ ↓
TimescaleDB Redis pub/sub
(almacenamiento) (tiempo real)
↓ ↓
API REST WebSocket server
↓ ↓
Dashboard (datos históricos + datos en vivo)
Separar datos históricos de datos en vivo
Este es el patrón clave. El dashboard necesita dos flujos de datos diferentes:
- Datos históricos: Cuando el usuario carga la página o cambia el rango temporal, se consultan datos almacenados en TimescaleDB via API REST
- Datos en vivo: Una vez cargada la vista, los nuevos datos llegan via WebSocket y se añaden al gráfico sin recarga
Esto permite que el dashboard muestre meses de historial con zoom eficiente Y se actualice en tiempo real con los datos nuevos.
Paso 1: Ingestión de datos MQTT
El primer componente es un servicio que se suscribe al broker MQTT y procesa los mensajes entrantes.
// ingestion-service.js (Node.js)
import mqtt from 'mqtt';
import { pool } from './db.js';
import { redis } from './redis.js';
const client = mqtt.connect(process.env.MQTT_BROKER_URL, {
username: process.env.MQTT_USER,
password: process.env.MQTT_PASS,
clean: false,
clientId: 'dashboard-ingestion-01'
});
client.subscribe('devices/+/telemetry', { qos: 1 });
client.on('message', async (topic, payload) => {
const deviceId = topic.split('/')[1];
const data = JSON.parse(payload.toString());
// 1. Almacenar en TimescaleDB
await pool.query(
`INSERT INTO telemetry (device_id, timestamp, temperature, humidity, battery)
VALUES ($1, NOW(), $2, $3, $4)`,
[deviceId, data.temperature, data.humidity, data.battery]
);
// 2. Publicar en Redis para WebSocket broadcast
await redis.publish('realtime:telemetry', JSON.stringify({
deviceId,
timestamp: Date.now(),
...data
}));
// 3. Evaluar reglas de alerta
await evaluateAlerts(deviceId, data);
});
Este servicio hace tres cosas con cada mensaje: lo almacena para consultas históricas, lo publica en Redis para que el WebSocket server lo transmita a los dashboards conectados, y evalúa si dispara alguna alerta.
Paso 2: WebSocket server
El WebSocket server mantiene conexiones persistentes con los dashboards y les envía datos nuevos en tiempo real.
// websocket-server.js
import { WebSocketServer } from 'ws';
import { redis } from './redis.js';
const wss = new WebSocketServer({ port: 8080 });
const subscriber = redis.duplicate();
// Suscribirse al canal de Redis
await subscriber.subscribe('realtime:telemetry');
subscriber.on('message', (channel, message) => {
const data = JSON.parse(message);
// Broadcast a todos los clientes conectados
// (en producción, filtrar por las suscripciones de cada cliente)
wss.clients.forEach(client => {
if (client.readyState === 1) {
client.send(message);
}
});
});
wss.on('connection', (ws, req) => {
// Autenticación
const token = new URL(req.url, 'http://localhost').searchParams.get('token');
if (!verifyToken(token)) {
ws.close(4001, 'Unauthorized');
return;
}
// Heartbeat para detectar conexiones muertas
ws.isAlive = true;
ws.on('pong', () => { ws.isAlive = true; });
});
// Ping periódico para detectar desconexiones
setInterval(() => {
wss.clients.forEach(ws => {
if (!ws.isAlive) return ws.terminate();
ws.isAlive = false;
ws.ping();
});
}, 30000);
WebSocket vs Server-Sent Events (SSE)
- WebSocket: Bidireccional. Úsalo si el dashboard envía comandos al servidor (filtros dinámicos, control de dispositivos)
- SSE: Unidireccional servidor-cliente. Más simple, funciona mejor con proxies HTTP. Úsalo si el dashboard solo recibe datos
Para la mayoría de dashboards IoT, SSE es suficiente y más simple de implementar. WebSocket es necesario cuando hay interacción bidireccional frecuente.
Paso 3: API REST para datos históricos
El dashboard necesita una API para cargar datos históricos cuando el usuario cambia el rango temporal o aplica filtros.
// api.js (Fastify)
app.get('/api/telemetry/:deviceId', async (req, reply) => {
const { deviceId } = req.params;
const { from, to, interval } = req.query;
// Downsampling automático según el rango temporal
const bucket = calculateBucket(from, to);
const result = await pool.query(`
SELECT
time_bucket($1, timestamp) AS time,
AVG(temperature) as temperature,
AVG(humidity) as humidity,
MIN(battery) as battery
FROM telemetry
WHERE device_id = $2
AND timestamp BETWEEN $3 AND $4
GROUP BY time
ORDER BY time ASC
`, [bucket, deviceId, from, to]);
return result.rows;
});
function calculateBucket(from, to) {
const diffHours = (new Date(to) - new Date(from)) / 3600000;
if (diffHours <= 1) return '1 minute';
if (diffHours <= 24) return '5 minutes';
if (diffHours <= 168) return '1 hour';
if (diffHours <= 720) return '6 hours';
return '1 day';
}
El truco del downsampling es fundamental: cuando el usuario ve un año de datos, no necesita 525.600 puntos (uno por minuto). Necesita ~365 puntos (uno por día). TimescaleDB tiene time_bucket nativo que hace esto eficientemente.
Paso 4: Frontend React
Hook para datos en tiempo real
// useRealtimeData.js
import { useEffect, useCallback, useRef } from 'react';
import { useQueryClient } from '@tanstack/react-query';
export function useRealtimeData(deviceId) {
const queryClient = useQueryClient();
const wsRef = useRef(null);
useEffect(() => {
const ws = new WebSocket(
`${WS_URL}?token=${getToken()}&device=${deviceId}`
);
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
// Actualizar la cache de TanStack Query
queryClient.setQueryData(
['telemetry', deviceId],
(old) => old ? [...old.slice(-999), data] : [data]
);
};
ws.onclose = () => {
// Reconexión automática con backoff exponencial
setTimeout(() => reconnect(), getBackoff());
};
wsRef.current = ws;
return () => ws.close();
}, [deviceId]);
}
Componente de gráfica con datos en vivo
// TimeSeriesChart.jsx
import { useQuery } from '@tanstack/react-query';
import { LineChart, Line, XAxis, YAxis, Tooltip } from 'recharts';
import { useRealtimeData } from './useRealtimeData';
function TimeSeriesChart({ deviceId, metric, from, to }) {
// Datos históricos
const { data: historicalData } = useQuery({
queryKey: ['telemetry', deviceId, from, to],
queryFn: () => fetchTelemetry(deviceId, from, to),
});
// Datos en tiempo real (se añaden al final)
useRealtimeData(deviceId);
return (
<LineChart data={historicalData} width={800} height={300}>
<XAxis dataKey="time" />
<YAxis />
<Tooltip />
<Line
type="monotone"
dataKey={metric}
stroke="#5dd3b3"
dot={false}
isAnimationActive={false}
/>
</LineChart>
);
}
Nota el isAnimationActive={false}: en dashboards con actualizaciones frecuentes, las animaciones de transición bloquean el render y degradan el rendimiento.
Paso 5: Sistema de alertas
// alerts.js
async function evaluateAlerts(deviceId, data) {
const rules = await getAlertRules(deviceId);
for (const rule of rules) {
const value = data[rule.metric];
let triggered = false;
switch (rule.condition) {
case 'above': triggered = value > rule.threshold; break;
case 'below': triggered = value < rule.threshold; break;
case 'equals': triggered = value === rule.threshold; break;
}
if (triggered) {
await createAlert({
deviceId,
ruleId: rule.id,
value,
threshold: rule.threshold,
severity: rule.severity,
timestamp: Date.now()
});
// Notificar via los canales configurados
await notify(rule.channels, {
device: deviceId,
metric: rule.metric,
value,
threshold: rule.threshold
});
}
}
}
Optimizaciones de rendimiento
1. Throttling de actualizaciones
Si un dispositivo envía datos cada 100ms, no necesitas actualizar el dashboard 10 veces por segundo. Agrupa actualizaciones:
// En el WebSocket server
const buffer = new Map();
subscriber.on('message', (channel, message) => {
const data = JSON.parse(message);
buffer.set(data.deviceId, data);
});
// Flush cada 200ms
setInterval(() => {
if (buffer.size > 0) {
const batch = Array.from(buffer.values());
broadcast(JSON.stringify({ type: 'batch', data: batch }));
buffer.clear();
}
}, 200);
2. Virtualización de listas
Si tienes 1.000 dispositivos, no renderices 1.000 filas. Usa virtualización (react-window o react-virtual) para renderizar solo las visibles.
3. Web Workers para procesamiento pesado
Si necesitas calcular estadísticas o detectar anomalías en el frontend, hazlo en un Web Worker para no bloquear el hilo principal.
4. Canvas en lugar de SVG para muchos puntos
Recharts usa SVG por defecto, que se degrada con más de 5.000 puntos. Para series temporales de alta resolución, usa una librería basada en Canvas (como uPlot o lightweight-charts).
Despliegue y operación
Docker Compose para desarrollo
services:
mqtt:
image: emqx/emqx:latest
ports: ["1883:1883", "8083:8083"]
timescaledb:
image: timescale/timescaledb:latest-pg16
environment:
POSTGRES_PASSWORD: dev
ports: ["5432:5432"]
redis:
image: redis:7-alpine
ports: ["6379:6379"]
ingestion:
build: ./services/ingestion
depends_on: [mqtt, timescaledb, redis]
api:
build: ./services/api
ports: ["3000:3000"]
depends_on: [timescaledb, redis]
dashboard:
build: ./frontend
ports: ["5173:5173"]
Monitorización del propio dashboard
Usa Prometheus para monitorizar:
- Número de conexiones WebSocket activas
- Latencia de escritura en TimescaleDB
- Mensajes MQTT procesados por segundo
- Tiempo de respuesta de la API
- Uso de memoria del WebSocket server
Alternativas: Grafana vs Custom
Usa Grafana si:
- El dashboard es para uso interno técnico
- TimescaleDB/InfluxDB es tu fuente de datos principal
- No necesitas UX personalizada
- Quieres estar operativo en días, no semanas
Construye custom si:
- El dashboard es para usuarios finales
- Necesitas branding y UX específica
- Tienes interacciones complejas (comandos a dispositivos, workflows)
- Necesitas funcionalidades que Grafana no soporta
En muchos proyectos, la mejor solución es ambas: Grafana para el equipo de ops y un dashboard custom para los usuarios de negocio.
Conclusión
Construir un dashboard IoT en tiempo real no es trivial, pero con el stack correcto y los patrones adecuados (separación histórico/real-time, downsampling, throttling), puedes tener un sistema robusto y performante.
Los puntos clave:
- Separa datos históricos (API REST + TimescaleDB) de datos en vivo (WebSocket + Redis pub/sub)
- Implementa downsampling automático según el rango temporal
- Usa throttling para no saturar el frontend con actualizaciones
- Diseña el sistema de alertas como componente independiente
- Monitoriza el propio dashboard como monitorizar cualquier sistema en producción
Si necesitas ayuda construyendo tu dashboard en tiempo real o tu plataforma IoT industrial, en Soamee llevamos años haciéndolo. Agenda una consultoría gratuita y vemos tu caso.