← Proyectos/dashboard-tiempo-real
Building💻 Full Stack

Dashboard en Tiempo Real con React

Dashboard interactivo Next.js con WebSockets para métricas en vivo desde CloudWatch y DynamoDB Streams.

Next.jsReactTypeScriptWebSocketsNode.jsCloudWatchDynamoDB Streams

Problema & Solución

Problema

Un equipo de operaciones necesita monitorear métricas de su plataforma AWS en tiempo real (CPU, requests/s, errores, latencia p95) sin refrescar la página. Los datos vienen de CloudWatch y DynamoDB. La solución debe actualizar el dashboard automáticamente cada vez que hay nuevos datos.

Solución

Frontend Next.js con componentes de gráficos en tiempo real. WebSocket server (Node.js) que actúa como proxy entre el browser y las APIs de AWS: consulta CloudWatch Metrics cada 60 segundos y escucha DynamoDB Streams en tiempo real. El cliente usa un hook personalizado useMetricsStream que gestiona la conexión WebSocket, reconexión automática y el buffer de datos.

Diagrama de Arquitectura


  Browser (Next.js)
     │
     │ WebSocket (ws://)
     ▼
  ┌────────────────────────────────────────────────────┐
  │  WebSocket Server (Node.js)                        │
  │                                                    │
  │  ┌─────────────────┐    ┌──────────────────────┐   │
  │  │ CloudWatch      │    │  DynamoDB Streams    │   │
  │  │ Poller (60s)    │    │  Listener (real-time)│   │
  │  └────────┬────────┘    └──────────┬───────────┘   │
  │           │                        │                │
  │           └──────────┬─────────────┘                │
  │                      ▼                              │
  │             ┌─────────────────┐                     │
  │             │ Broadcast a     │                     │
  │             │ clientes WS     │                     │
  │             │ suscritos       │                     │
  │             └─────────────────┘                     │
  └────────────────────────────────────────────────────┘
         │                          │
         ▼                          ▼
  ┌─────────────┐          ┌─────────────────┐
  │  CloudWatch │          │  DynamoDB       │
  │  Metrics    │          │  Streams        │
  │  (CPU, req, │          │  (nuevos items) │
  │   latencia) │          └─────────────────┘
  └─────────────┘

  Frontend State Machine (useMetricsStream):
  DISCONNECTED ──connect──▶ CONNECTING ──open──▶ CONNECTED
       ▲                                              │
       └──────────── error/close (backoff retry) ◀───┘

Implementación

1

Hook useMetricsStream — gestión del WebSocket en React

El hook gestiona el ciclo de vida completo de la conexión WebSocket: conectar, recibir datos, reconectar con exponential backoff (1s, 2s, 4s, 8s, max 30s) ante desconexiones. Mantiene un buffer circular de los últimos 60 puntos de datos por métrica para renderizar la gráfica. Cleanup en el useEffect garantiza que el socket se cierra cuando el componente se desmonta.

2

CloudWatch Metrics Poller (server-side)

El WebSocket server consulta CloudWatch GetMetricData cada 60 segundos para las métricas configuradas (CPU, RequestCount, TargetResponseTime, HTTPCode_ELB_5XX_Count). Usa el SDK v3 de AWS con IAM Role (sin credentials hardcodeadas). Los datos se normalizan y se brodacastean a todos los clientes WebSocket suscritos a esa métrica.

3

DynamoDB Streams — eventos en tiempo real

DynamoDB Streams captura cada INSERT/MODIFY/REMOVE en la tabla. El servidor usa el SDK para leer los stream records con getShardIterator + getRecords en polling (o con Kinesis Client Library para mayor robustez). Cada nuevo item se parsea y se brodacastea al dashboard en <1 segundo desde el evento original.

4

Renderizado de gráficas con Recharts

Los componentes de gráfica usan Recharts (LineChart, AreaChart). Son 'use client' en Next.js (interactivos). Cada punto de dato nuevo se añade al estado con setData(prev => [...prev.slice(-59), newPoint]) — mantiene los últimos 60 puntos y el componente re-renderiza solo la parte que cambia gracias al virtual DOM de React.

Tech Stack

Frontend

Next.js 16React 19TypeScriptRechartsTailwind CSS

WebSocket Server

Node.js 20ws (WebSocket library)TypeScript

AWS SDKs

@aws-sdk/client-cloudwatch@aws-sdk/client-dynamodb-streams

Infraestructura

ECS Fargate (WS server)Next.js en VercelIAM Roles

Decisiones Técnicas

WebSockets vs Server-Sent Events (SSE) vs Polling

Elegido

WebSockets

Alternativas

  • Server-Sent Events — más simple, nativo en browser, pero solo server→client
  • Long Polling — compatible con cualquier servidor HTTP, más latencia
  • GraphQL Subscriptions — over WebSockets, más estructura pero más overhead

Razón

WebSockets permiten comunicación bidireccional — el cliente puede suscribirse a métricas específicas y el server solo envía lo relevante. SSE es unidireccional (server → client) y más simple pero no permite que el cliente filtre qué datos quiere. Polling (fetch cada N segundos) añade latencia y carga innecesaria.

Snippets de Código

TypeScript — Hook useMetricsStream con reconexión automáticatypescript
import { useEffect, useRef, useState, useCallback } from "react";

type MetricPoint = { timestamp: number; value: number };
type ConnectionState = "disconnected" | "connecting" | "connected" | "error";

const MAX_BACKOFF_MS = 30_000;

export function useMetricsStream(metricName: string, maxPoints = 60) {
  const [data, setData] = useState<MetricPoint[]>([]);
  const [state, setState] = useState<ConnectionState>("disconnected");

  const wsRef = useRef<WebSocket | null>(null);
  const backoffRef = useRef(1000);
  const timeoutRef = useRef<ReturnType<typeof setTimeout>>();

  const connect = useCallback(() => {
    const ws = new WebSocket(
      `${process.env.NEXT_PUBLIC_WS_URL}/metrics`
    );
    wsRef.current = ws;
    setState("connecting");

    ws.onopen = () => {
      setState("connected");
      backoffRef.current = 1000; // reset backoff al conectar
      ws.send(JSON.stringify({ action: "subscribe", metric: metricName }));
    };

    ws.onmessage = (event: MessageEvent) => {
      const point: MetricPoint = JSON.parse(event.data as string);
      setData((prev) => [...prev.slice(-(maxPoints - 1)), point]);
    };

    ws.onerror = () => setState("error");

    ws.onclose = () => {
      setState("disconnected");
      // Exponential backoff con jitter
      const delay = Math.min(backoffRef.current, MAX_BACKOFF_MS);
      backoffRef.current = Math.min(backoffRef.current * 2, MAX_BACKOFF_MS);
      timeoutRef.current = setTimeout(connect, delay + Math.random() * 1000);
    };
  }, [metricName, maxPoints]);

  useEffect(() => {
    connect();
    return () => {
      clearTimeout(timeoutRef.current);
      wsRef.current?.close(1000, "Component unmounted");
    };
  }, [connect]);

  return { data, state };
}
Node.js — CloudWatch Metrics Poller (WebSocket Server)typescript
import { CloudWatchClient, GetMetricDataCommand } from "@aws-sdk/client-cloudwatch";
import { WebSocketServer, WebSocket } from "ws";

const cw = new CloudWatchClient({ region: "us-east-1" });
const wss = new WebSocketServer({ port: 8080 });

// Map de metric → Set de clients suscritos
const subscriptions = new Map<string, Set<WebSocket>>();

wss.on("connection", (ws) => {
  ws.on("message", (raw) => {
    const msg = JSON.parse(raw.toString()) as { action: string; metric: string };
    if (msg.action === "subscribe") {
      if (!subscriptions.has(msg.metric)) {
        subscriptions.set(msg.metric, new Set());
      }
      subscriptions.get(msg.metric)!.add(ws);
    }
  });

  ws.on("close", () => {
    // Limpia suscripciones del cliente desconectado
    for (const clients of subscriptions.values()) {
      clients.delete(ws);
    }
  });
});

async function pollCloudWatch(): Promise<void> {
  const now = new Date();
  const start = new Date(now.getTime() - 5 * 60 * 1000); // últimos 5 min

  const { MetricDataResults } = await cw.send(
    new GetMetricDataCommand({
      StartTime: start,
      EndTime: now,
      MetricDataQueries: [
        {
          Id: "cpu",
          MetricStat: {
            Metric: {
              Namespace: "AWS/ECS",
              MetricName: "CPUUtilization",
              Dimensions: [{ Name: "ServiceName", Value: "portfolio-api" }],
            },
            Period: 60,
            Stat: "Average",
          },
        },
      ],
    })
  );

  for (const result of MetricDataResults ?? []) {
    const clients = subscriptions.get(result.Id ?? "");
    if (!clients?.size) continue;

    const points = (result.Timestamps ?? []).map((ts, i) => ({
      timestamp: ts.getTime(),
      value: result.Values?.[i] ?? 0,
    }));

    for (const point of points) {
      const payload = JSON.stringify(point);
      for (const client of clients) {
        if (client.readyState === WebSocket.OPEN) {
          client.send(payload);
        }
      }
    }
  }
}

// Poll cada 60 segundos
setInterval(pollCloudWatch, 60_000);
pollCloudWatch(); // ejecuta inmediatamente al arrancar