SSE Real-time Event Push
Logged-in web pages receive one-way events via GET /api/events. SSE updates collection and protocol states; system health metrics are still read via the corresponding REST endpoints. A heartbeat only proves the connection received a message — it does not prove sensor collection, rule execution, or MQTT reporting is normal.
Events and Data
| Event | Content |
|---|---|
sensor-data | PeriphExec collection results; updates pages per the actual data-source fields and quality |
modbus-data | Array of Modbus data records; large arrays are sent in complete-record chunks |
mqtt-status | MQTT status JSON |
modbus-status | Modbus status JSON |
resync | Recovery hint when chunked push fails; re-fetch the current snapshot via REST |
heartbeat | Text ping; sent when the maintenance period reaches 60 seconds |
Business messages are JSON strings in event.data and must be JSON.parsed first. Named events use addEventListener; onmessage does not receive all named events. Sensor fields vary with configuration — top-level temperature and humidity are not guaranteed to exist. Invalid, expired, or failed reads must not be replaced with zero values; valid zeros can display normally.
The Modbus single-packet budget is 512 bytes. Oversized record arrays are sent on record boundaries; a single record cannot be chunked, and if the send queue fails, some records may already have been received — then request resync. Clients should read back snapshots to fill gaps; do not stitch these arrays into one raw JSON document, and do not rely on event IDs to replay all missed messages.
Connection and Memory Limits
The connection budget comes from ResourceProfile::TCP_SSE_BUDGET:
| Chip | SSE connection budget |
|---|---|
| ESP32, ESP32-C3, ESP32-C6 | 1 |
| ESP32-S3 | 2 |
When the largest contiguous block of internally byte-addressable DRAM is below 8192 bytes, or memory protection is at SEVERE/CRITICAL, new connections return 503 and business broadcasts pause. When the budget is full, new connections are usually rejected; a concurrency race during connection setup still has a fallback that evicts old connections. Resource gating runs before the authentication check, so an unauthenticated request may also get 503 first; when resources allow but the session is invalid, 401 is returned.
The server updates connection activity with positive-length TCP ACKs and cleans up connections with no activity acknowledgment for 120 seconds; enqueueing a heartbeat does not refresh that time. Actual sends are also subject to the maintenance schedule — 60 seconds is not a strict timing or business-freshness guarantee. Memory and connection overhead includes TCP buffers, message queues, and client tracking; it cannot be estimated as a fixed 500 bytes.
Browser Integration
A same-origin EventSource('/api/events') automatically carries the device session cookie. The minimal example below waits 5 seconds before reconnecting after an error, reminds you to re-fetch the business snapshot on reconnect or resync, and cancels timers after leaving the page:
let source = null;
let retryTimer = null;
let stopped = false;
function recoverSnapshot(reason) {
// The integrator serially reads the required REST data here,
// verifying quality and sampling time.
console.info('Business snapshot refresh needed:', reason);
}
function connectEvents() {
if (stopped || source) return;
const current = new EventSource('/api/events');
source = current;
current.addEventListener('sensor-data', event => {
if (source !== current || stopped) return;
try {
const payload = JSON.parse(event.data);
console.log('This collection:', payload);
} catch (error) {
recoverSnapshot('invalid_sensor_json');
}
});
current.addEventListener('resync', () => recoverSnapshot('server_resync'));
current.onopen = () => {
if (source === current && !stopped) recoverSnapshot('connected');
};
current.onerror = () => {
if (source !== current || stopped) return;
current.close();
source = null;
if (retryTimer !== null) clearTimeout(retryTimer);
retryTimer = setTimeout(() => {
retryTimer = null;
connectEvents();
}, 5000);
};
}
function stopEvents() {
stopped = true;
if (retryTimer !== null) clearTimeout(retryTimer);
retryTimer = null;
if (source) source.close();
source = null;
}
window.addEventListener('pagehide', stopEvents);
connectEvents();Re-establish the connection and refresh snapshots when the page resumes. The project's existing web client also provides exponential backoff and connection-stall detection; prefer reusing AppState event registration and page-leave cleanup when integrating.
Troubleshooting
| Symptom | Check |
|---|---|
401 | Log in again and confirm the same-origin session cookie |
503 or connection closed | Close extra tabs, watch internal DRAM and protection state, keep backoff |
| Only heartbeats, no business updates | Check enabled collection rules, protocol tasks, and data validity |
| Data missing after reconnect | Read back the business snapshot serially; heartbeats do not fill business data |
| Named events not received | Use the corresponding addEventListener and parse event.data |
Using SSE does not require running /api/mqtt/test first; that test endpoint stages the config, disconnects, and rebuilds MQTT. Use the status endpoints for ordinary reads and page recovery. Verify the current source boundaries against old device versions separately; endpoint connectivity does not mean physical or long-term-stability acceptance either.
Related: API Reference, Health Monitor, MQTT Configuration.

