Saltar al contenido principal
Volver al blog
IoT Dashboard WebSocket React

Cómo construir un dashboard IoT en tiempo real

Tutorial paso a paso para construir un dashboard IoT en tiempo real. Stack técnico, WebSocket, series temporales, React, alertas y arquitectura de datos.

JM
Javier Manzano
CEO & Co-founder • 12 de junio de 2026

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íaPara qué
React 18+Framework UI con Concurrent Features
Recharts o VisxGráficas de series temporales performantes
D3.jsVisualizaciones custom complejas
TanStack QueryGestión de estado servidor + cache
Socket.IO o native WebSocketDatos en tiempo real
Tailwind CSSEstilos rápidos y consistentes
ZustandEstado global ligero

Backend

TecnologíaPara qué
Node.js + FastifyAPI REST + WebSocket server
RedisCache + pub/sub para broadcast
TimescaleDBBase de datos de series temporales
PostgreSQLMetadatos, configuraciones, usuarios
MQTT broker (EMQX/Mosquitto)Recepción de datos de dispositivos

Infraestructura

TecnologíaPara qué
Docker + Docker ComposeDesarrollo local y despliegue
KubernetesProducción escalable
NginxReverse proxy + WebSocket upgrade
Prometheus + GrafanaMonitorizació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:

  1. Datos históricos: Cuando el usuario carga la página o cambia el rango temporal, se consultan datos almacenados en TimescaleDB via API REST
  2. 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:

  1. Separa datos históricos (API REST + TimescaleDB) de datos en vivo (WebSocket + Redis pub/sub)
  2. Implementa downsampling automático según el rango temporal
  3. Usa throttling para no saturar el frontend con actualizaciones
  4. Diseña el sistema de alertas como componente independiente
  5. 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.

No te pierdas nada

JM

Javier Manzano

CEO & Co-founder en Soamee

Apasionado por la tecnología y el desarrollo de software. Comparto conocimientos y experiencias para ayudar a otros desarrolladores a crecer.

¿Te ha gustado este artículo?

Si necesitas ayuda con tu proyecto de desarrollo, estamos aquí para ti.

Agenda call gratuita →