Back to Blog
IoT Engineering

IoT Data Pipeline: From Raw Sensor Reading to Live Dashboard in Under 100ms

A sub-100ms sensor-to-dashboard latency is achievable in production — but it requires careful architecture at every layer from ESP32 firmware to WebSocket push. This walkthrough covers the full stack with real latency budgets.

April 15, 2024
13 min read
IoT Data PipelineESP32MQTTInfluxDB

IoT Data Pipeline: From Raw Sensor Reading to Live Dashboard in Under 100ms

"The dashboard shows data from 5 minutes ago" is a complaint we hear constantly from clients inheriting IoT systems built without latency in mind. A sub-100ms sensor-to-dashboard pipeline is achievable — it just requires understanding where time goes at each layer and making deliberate choices.

The Full Stack

[ESP32 Sensor]         ~1ms  sampling + JSON encode
      ↓ WiFi + MQTT TLS
[AWS IoT Core]         ~8ms  broker receive + rules engine
      ↓ IoT Rule → Lambda / direct MQTT subscription
[Node.js Backend]      ~2ms  parse + fan-out
      ├─→ [InfluxDB]   ~5ms  async write (non-blocking)
      └─→ [WebSocket]  ~1ms  push to subscribed dashboards
            ↓ WebSocket frame
[React Dashboard]      ~5ms  React state update + render

Total budget: ~22ms (LAN) to ~80ms (cross-region internet)

Layer 1: ESP32 Firmware — Minimize On-Device Latency

The most common firmware latency killer: formatting a JSON string character-by-character with sprintf. Use a fixed pre-allocated buffer.

#include 
#include 
#include 

// Pre-allocate JSON document (stack-allocated, no heap fragmentation) StaticJsonDocument<256> doc; char pubBuffer[256];

void publishReading(float temperature, float humidity, uint8_t battery) { doc.clear() doc["deviceId"] = DEVICE_ID; doc["temperature"] = serialized(String(temperature, 2)); doc["humidity"] = serialized(String(humidity, 1)); doc["battery"] = battery; doc["ts"] = millis(); // relative — server adds absolute timestamp

size_t len = serializeJson(doc, pubBuffer, sizeof(pubBuffer));

// QoS 0 for telemetry: no ack overhead, lowest latency mqttClient.publish("devices/" DEVICE_ID "/telemetry", pubBuffer, len); }

Key ESP32 firmware decisions for latency:

  • QoS 0 publish: eliminates PUBACK round-trip (~8ms on LTE)
  • Pre-allocated JSON buffers: no heap allocation on every reading
  • WiFi: use WIFI_PS_NONE (disable power save) for 10-20ms latency reduction in exchange for ~30mA extra draw
  • Publish interval: 100ms minimum for real-time feel; 500ms–1s for most dashboards
  • Layer 2: Node.js MQTT Subscriber — Fan-Out Without Blocking

    The backend subscribes to all device telemetry topics and fans data to both the database write path and the WebSocket push path. The critical rule: never await the database write before pushing to WebSocket.

    // server.js — MQTT to WebSocket fan-out
    const mqtt = require('mqtt')
    const { WebSocketServer } = require('ws')
    const { InfluxDB, Point } = require('@influxdata/influxdb-client')

    const influxWriteApi = new InfluxDB({ url: process.env.INFLUXDB_URL, token: process.env.INFLUXDB_TOKEN, }).getWriteApi('org', 'sensors', 'ms')

    // Batch InfluxDB writes: flush every 500ms or 1000 points influxWriteApi.writeOptions = { batchSize: 1000, flushInterval: 500, maxRetries: 3, }

    const wss = new WebSocketServer({ port: 8080 })

    // Track subscriptions: deviceId → Set of WebSocket clients const subscriptions = new Map()

    wss.on('connection', (ws) => { ws.on('message', (msg) => { const { action, deviceId } = JSON.parse(msg) if (action === 'subscribe') { if (!subscriptions.has(deviceId)) subscriptions.set(deviceId, new Set()) subscriptions.get(deviceId).add(ws) ws.deviceSubscriptions = ws.deviceSubscriptions || new Set() ws.deviceSubscriptions.add(deviceId) } }) ws.on('close', () => { ws.deviceSubscriptions?.forEach(id => subscriptions.get(id)?.delete(ws)) }) })

    const mqttClient = mqtt.connect(process.env.MQTT_BROKER_URL, { // TLS options... })

    mqttClient.subscribe('devices/+/telemetry')

    mqttClient.on('message', (topic, buffer) => { const deviceId = topic.split('/')[1] let payload try { payload = JSON.parse(buffer) } catch { return }

    // 1. Push to WebSocket immediately (synchronous, ~0.1ms) const subscribers = subscriptions.get(deviceId) if (subscribers?.size) { const frame = JSON.stringify({ deviceId, ...payload, serverTs: Date.now() }) subscribers.forEach(ws => { if (ws.readyState === ws.OPEN) ws.send(frame) }) }

    // 2. Write to InfluxDB asynchronously (does NOT block fan-out) const point = new Point('environment') .tag('deviceId', deviceId) .floatField('temperature', payload.temperature) .floatField('humidity', payload.humidity) .intField('battery', payload.battery) .timestamp(new Date()) influxWriteApi.writePoint(point) // non-blocking, batched internally })

    Layer 3: React Dashboard — WebSocket State Management

    // hooks/useDeviceStream.ts
    import { useEffect, useRef, useCallback, useState } from 'react'

    interface Reading { deviceId: string temperature: number humidity: number battery: number serverTs: number }

    export function useDeviceStream(deviceIds: string[]) { const [readings, setReadings] = useState>({}) const wsRef = useRef(null)

    const connect = useCallback(() => { const ws = new WebSocket(process.env.NEXT_PUBLIC_WS_URL!) wsRef.current = ws

    ws.onopen = () => { deviceIds.forEach(id => ws.send(JSON.stringify({ action: 'subscribe', deviceId: id })) ) }

    ws.onmessage = (event) => { const reading: Reading = JSON.parse(event.data) setReadings(prev => ({ ...prev, [reading.deviceId]: reading })) }

    ws.onclose = () => setTimeout(connect, 3000) // reconnect ws.onerror = () => ws.close() }, [deviceIds])

    useEffect(() => { connect() return () => wsRef.current?.close() }, [connect])

    return readings }

    React rendering optimization: Use useMemo and React.memo on individual gauge components so a reading update for device A doesn't re-render device B's gauge. At 50+ devices, unoptimized rendering becomes the bottleneck.

    Latency Budget Analysis

    | Stage | LAN (same region) | Cross-region (e.g., EU device, US cloud) | |-------|-------------------|------------------------------------------| | ESP32 WiFi + MQTT publish | 5–15ms | 5–15ms | | IoT Core broker processing | 2–5ms | 2–5ms | | Network (device → cloud) | 1–5ms | 50–150ms | | Node.js MQTT → WebSocket | <1ms | <1ms | | Network (cloud → browser) | 1–10ms | 20–80ms | | React state update + render | 3–8ms | 3–8ms | | Total | 12–44ms | 80–259ms |

    For cross-region deployments, deploy a regional Node.js relay close to the devices. The relay receives MQTT locally and forwards aggregates to the central cloud. This cuts the device → cloud leg from 150ms to 5ms.

    Backpressure Handling

    When sensor message rate exceeds WebSocket push capacity (rare but possible at 1000+ devices):

    // Rate-limit per-device WebSocket pushes to max 10/second
    const lastPushed = new Map()

    function pushIfNotThrottled(ws, deviceId, frame) { const now = Date.now() const last = lastPushed.get(deviceId) || 0

    if (now - last >= 100) { // 10 Hz max per device ws.send(frame) lastPushed.set(deviceId, now) } }

    For InfluxDB write backpressure, the client library handles batching internally. Monitor influxWriteApi.writeOptions.maxBufferLines and alert if the buffer approaches capacity.

    Buffering Strategies for Offline Recovery

    If the MQTT subscriber process restarts, messages published during downtime are lost (QoS 0). For QoS 1 subscriptions, the broker queues messages during reconnection. Choose based on your acceptable data loss window:

  • Analytics dashboards: QoS 0 is fine — missing 10 seconds of data is invisible
  • Billing/metering data: QoS 1 required — every reading must land in InfluxDB
  • For the storage layer details, see [Time-Series Databases for IoT](/blog/timeseries-databases-iot-influxdb-vs-timestream). For the Flutter mobile dashboard side of this architecture, see [Flutter IoT Real-Time Dashboard Architecture](/blog/flutter-iot-real-time-dashboard-architecture).

    Need help building a low-latency IoT data pipeline? [Contact Code Caracal](/contact) — we've shipped these systems for clients across 15+ countries.

    Written by CodeCaracal Engineering

    We write from production experience — every technique in our articles has been deployed to real clients. No academic theory.

    More Articles

    Business · 12 min read

    IoT Device Compliance: FCC, CE, and Product Certification Guide for Hardware Startups

    Business · 11 min read

    What to Look for When Hiring an IoT Development Partner: 8 Critical Criteria

    Business · 11 min read

    IoT MVP to Production: Realistic Timeline and Budget for Hardware Startups

    Business · 11 min read

    IoT Development Agency vs Building In-House: A Decision Framework for Founders

    IoT Dashboard · 13 min read

    Next.js IoT Analytics Dashboard: From Sensor Data to Production App

    Business · 11 min read

    How Much Does It Cost to Build an IoT Product in 2024? A Realistic Breakdown

    IoT Dashboard · 11 min read

    IoT Dashboard UX: Design Principles for Industrial Monitoring Interfaces

    IoT Dashboard · 12 min read

    Node.js WebSocket Server: The Real-Time Backend for IoT Dashboards

    Cloud & DevOps · 12 min read

    Containerizing IoT Backend Services with Docker: From Dev to Production

    IoT Dashboard · 14 min read

    Grafana + InfluxDB IoT Monitoring: Complete Production Setup Guide

    IoT Dashboard · 12 min read

    Building Real-Time IoT Dashboards with React and Recharts

    Cloud & DevOps · 13 min read

    CI/CD for Embedded Firmware: Automated Build, Test, and OTA Release Pipeline

    Mobile Development · 12 min read

    Flutter Offline-First IoT Apps: Hive + Sync Architecture That Works in the Field

    Cloud & DevOps · 14 min read

    Terraform for IoT Infrastructure: Provisioning AWS IoT Core, Lambda, and InfluxDB as Code

    Mobile Development · 10 min read

    Flutter IoT Alerts: Firebase Push Notifications for Device Events

    Cloud & DevOps · 12 min read

    Deploying IoT Backends on AWS: ECS Fargate vs Lambda vs EC2 Decision Guide

    Mobile Development · 11 min read

    Flutter + MQTT: Building Production IoT Mobile Apps That Scale

    Mobile Development · 13 min read

    Flutter BLE: Building a Bluetooth IoT Controller App from Scratch

    Cloud & DevOps · 13 min read

    AWS IoT Core vs Azure IoT Hub vs Google Cloud IoT: 2024 Honest Comparison

    IoT Engineering · 13 min read

    Kafka vs RabbitMQ for IoT: Choosing the Right Message Queue for High-Volume Telemetry

    IoT Engineering · 14 min read

    IoT System Testing: Unit, Integration, Hardware-in-the-Loop, and End-to-End

    IoT Engineering · 14 min read

    Predictive Maintenance with IoT Sensor Data: From Threshold to Machine Learning

    Embedded Systems · 14 min read

    IoT Bootloader Design: Secure Boot, A/B Partitions, and Reliable OTA Recovery

    IoT Engineering · 14 min read

    Multi-Tenant IoT Platform Architecture: Isolation, Scaling, and Data Partitioning

    Embedded Systems · 14 min read

    Memory Management in Embedded Firmware: Avoiding Heap Fragmentation and Stack Overflows

    IoT Engineering · 13 min read

    IoT Cost Optimization: How We Cut AWS IoT Bills by 60% Without Sacrificing Reliability

    IoT Engineering · 12 min read

    Edge Computing in IoT: When to Process On-Device vs In the Cloud

    IoT Engineering · 13 min read

    Digital Twins for IoT: Building a Virtual Mirror of Your Physical Devices

    Embedded Systems · 14 min read

    ESP32 Deep Sleep Mastery: Cutting Power Consumption from 240mA to 10µA

    IoT Engineering · 10 min read

    MQTT QoS 0, 1, and 2 Explained: Choosing the Right Level for IoT

    IoT Engineering · 14 min read

    IoT Monitoring and Observability: Metrics, Logs, and Distributed Tracing

    Embedded Systems · 14 min read

    Debugging Embedded Firmware: JTAG, GDB, Logic Analyzers, and Serial Tracing

    IoT Engineering · 12 min read

    WebSocket vs MQTT vs Server-Sent Events: Real-Time IoT Protocol Deep Dive

    Embedded Systems · 13 min read

    STM32 HAL vs Low-Level Drivers: When the Abstraction Costs You Too Much

    IoT Engineering · 13 min read

    Zero-Touch IoT Device Provisioning: Scaling from 10 to 100,000 Devices

    Embedded Systems · 13 min read

    UART vs SPI vs I2C: Choosing the Right Protocol for Sensor Integration

    IoT Engineering · 12 min read

    Real-Time IoT Alerting: From Simple Thresholds to ML Anomaly Detection

    Embedded Systems · 12 min read

    ESP32 Partition Table: Designing Flash Layout for Production Firmware

    IoT Engineering · 12 min read

    IoT Architecture Patterns: Hub-and-Spoke, Mesh, and Edge-Cloud Hybrid

    Embedded Systems · 13 min read

    IoT Battery Life Optimization: Engineering Devices That Last Years on a Single Charge

    IoT Engineering · 13 min read

    Time-Series Databases for IoT: InfluxDB vs TimescaleDB vs AWS Timestream

    Security · 14 min read

    Zero-Trust Security for Embedded IoT: Why Your Devices Are Probably Vulnerable

    Embedded Systems · 14 min read

    FreeRTOS on ESP32: Task Scheduling, Queues, and Resource Management for IoT

    IoT Engineering · 12 min read

    Building a Production IoT Gateway with Raspberry Pi and Node.js

    Embedded Systems · 13 min read

    ESP32 vs STM32: Choosing the Right Microcontroller for Your IoT Project

    Mobile Development · 10 min read

    Flutter + WebSocket: Building Real-Time IoT Dashboards That Don't Stutter

    IoT Engineering · 13 min read

    IoT Fleet Management at Scale: AWS IoT Core Device Registry and Provisioning

    IoT Engineering · 11 min read

    MQTT vs HTTP for IoT: Which Protocol Wins in Production?

    IoT Engineering · 12 min read

    ESP32 → MQTT → AWS IoT Core: The Production-Grade Architecture Guide

    Let's Build Together

    Got an IoT challenge?
    We've shipped it.

    Whether you need a fleet to track, a factory to monitor, or a farm to automate — our team has done it before and we'd love to build it with you. Typical response time: under 24 hours.

    No upfront commitment99.9% uptime SLANDA on requestFixed-price options