Document Version: 1.0
Last Updated: January 2026
Scope: Socket.IO namespaces, event bus patterns, client subscriptions, latency considerations
Nightscout's real-time capabilities are critical for timely glucose monitoring alerts. This audit examines the event-driven architecture, WebSocket implementation, and opportunities for improvement.
| Component | Technology | Purpose |
|---|---|---|
| Internal Event Bus | Node.js Stream | Inter-process communication |
| Legacy WebSocket | Socket.IO 4.5 | Client data updates |
| Storage Socket | Socket.IO 4.5 | API v3 CRUD events |
| Alarm Socket | Socket.IO 4.5 | Alert broadcasting |
Location: lib/bus.js
The event bus is a Node.js Stream that provides pub/sub functionality within the server process.
Implementation:
var Stream = require('stream');
function init (settings) {
var stream = new Stream;
stream.readable = true;
// Heartbeat ticker
busInterval = setInterval(function() {
stream.emit('tick', ictus());
}, settings.heartbeat * 1000);
stream.teardown = function () {
clearInterval(busInterval);
stream.emit('teardown');
};
return stream;
}| Event | Source | Subscribers | Data |
|---|---|---|---|
tick |
Bus (timer) | Data loader | { now, beat, interval } |
data-received |
API endpoints | Data loader | (none) |
data-loaded |
Data loader | Plugin system | (none) |
data-processed |
Plugin system | Runtime state | sbx |
notification |
Plugins, ack | Push notify, WebSocket | Notification object |
admin-notify |
Auth failures | Admin notifier | { title, message } |
teardown |
Server shutdown | All cleanup handlers | (none) |
storage-socket-create |
API v3 | Storage socket | { col, doc } |
storage-socket-update |
API v3 | Storage socket | { col, doc } |
storage-socket-delete |
API v3 | Storage socket | { col, identifier } |
┌─────────────┐
│ Timer │
│ (heartbeat) │
└──────┬──────┘
│ tick
▼
┌──────────────┐ ┌─────────────┐ ┌─────────────┐
│ API v1/v3 │────▶│ Event │────▶│ Data │
│ Endpoints │data-│ Bus │data-│ Loader │
└──────────────┘recv │ │loaded└─────────────┘
└──────┬──────┘ │
│ │
┌───────────────────┼───────────────────┼────────────────┐
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Plugin │ │ WebSocket │ │ Push │ │ Storage │
│ System │ │ Broadcast │ │ Notify │ │ Socket │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
| Event Trigger | Typical Interval | Latency |
|---|---|---|
| Heartbeat tick | 60 seconds (configurable) | <1ms |
| Data received | On API write | <1ms |
| Data processed | After tick + load | 100-500ms |
| Notification | On plugin alarm | <10ms |
| Issue | Impact | Recommendation |
|---|---|---|
| No event typing | Runtime errors | Add TypeScript definitions |
| No event validation | Data corruption | Add schema validation |
| Single-threaded | Scalability | Consider Redis pub/sub |
| No persistence | Lost events on crash | Add event sourcing |
| No replay | Debugging difficulty | Add event logging |
Location: lib/server/websocket.js
Initialization:
var io = require('socket.io')(server, {
// Default configuration
pingTimeout: 60000,
pingInterval: 25000
});
io.on('connection', function (socket) {
// Handle connection
});| Namespace | Path | Purpose | Auth Required |
|---|---|---|---|
| Default | / |
Legacy data updates | Optional |
| Storage | /storage |
Collection CRUD events | Yes |
| Alarm | /alarm |
Alert broadcasting | Yes |
Location: lib/server/websocket.js
Client Connection:
const socket = io('https://nightscout.example.com/', {
query: { token: 'access-token' }
});Server Events (outbound):
| Event | Payload | Trigger |
|---|---|---|
dataUpdate |
{ delta, ... } |
Data change |
alarm |
Notification object | Warning alarm |
urgent_alarm |
Notification object | Urgent alarm |
announcement |
Notification object | User announcement |
clear_alarm |
{} |
Alarm cleared |
connect |
(none) | Connection established |
Client Events (inbound):
| Event | Payload | Action |
|---|---|---|
authorize |
{ client, secret, token, history } |
Authenticate |
ack |
{ level, group, silenceTime } |
Acknowledge alarm |
Location: lib/api3/storageSocket.js
Subscription:
socket.emit('subscribe', {
accessToken: 'mytoken-abc123',
collections: ['entries', 'treatments'] // Optional filter
}, function(response) {
if (response.success) {
console.log('Subscribed to:', response.collections);
}
});Server Events:
| Event | Payload | Description |
|---|---|---|
create |
{ colName, doc } |
Document created |
update |
{ colName, doc } |
Document updated |
delete |
{ colName, identifier } |
Document deleted |
subscribed |
{ collections } |
Subscription confirmed |
Permission Mapping:
const permission = (col === 'settings')
? `api:${col}:admin`
: `api:${col}:read`;Location: lib/api3/alarmSocket.js
Subscription:
socket.emit('subscribe', {
accessToken: 'mytoken-abc123'
}, function(response) {
if (response.success) {
console.log('Subscribed to alarms');
}
});Server Events:
| Event | Payload | Level |
|---|---|---|
announcement |
Notification object | INFO |
alarm |
Notification object | WARN |
urgent_alarm |
Notification object | URGENT |
clear_alarm |
{} |
Clear |
Acknowledgment:
socket.on('ack', function(level, group, silenceTime) {
ctx.notifications.ack(level, group, silenceTime);
});Location: lib/client/index.js, lib/client/socket.js
Connection Flow:
- Page loads → Get server status
- Connect to default namespace
- Send
authorizewith token - Subscribe to data updates
- Handle real-time events
Event Handlers:
socket.on('dataUpdate', function(data) {
// Merge delta into local cache
receiveDData.mergeDataUpdate(data.delta, ...);
// Trigger chart update
chart.update();
});
socket.on('alarm', function(alarm) {
// Show alarm notification
client.showNotification(alarm);
// Play alarm sound
audio.play();
});Common Patterns:
- Connect to appropriate namespace
- Subscribe with access token
- Handle
dataUpdateor granular CRUD events - Reconnect on disconnect
Reconnection Strategy:
const socket = io(serverUrl, {
reconnection: true,
reconnectionDelay: 1000,
reconnectionDelayMax: 5000,
reconnectionAttempts: Infinity
});Configuration: HEARTBEAT environment variable (default: 60 seconds)
Flow:
Timer (every 60s)
↓ emit('tick')
Event Bus
↓
Data Loader (debounced)
↓ query MongoDB
↓ merge new data
↓ emit('data-loaded')
Plugin System
↓ process data
↓ check notifications
↓ emit('data-processed')
WebSocket
↓ broadcast to clients
Flow:
API POST /entries
↓ save to MongoDB
↓ emit('data-received')
Event Bus
↓ (immediate, bypasses debounce delay)
Data Loader
↓ ... same as above
Location: lib/data/calcdelta.js
Purpose: Calculate minimal update for WebSocket clients
Algorithm:
- Compare current data with last sent data
- Identify new, modified, deleted items
- Create delta object with changes only
- Track last sent timestamp per client
Delta Object:
{
delta: true,
lastUpdated: 1595001000000,
sgvs: [/* new/changed entries */],
treatments: [/* new/changed treatments */],
mbgs: [],
cals: [],
profiles: [],
devicestatus: []
}Typical Path (CGM → Dashboard):
| Stage | Typical Latency | Notes |
|---|---|---|
| CGM → Uploader | 5 minutes | CGM reading interval |
| Uploader → API | 100-500ms | Network + API processing |
| API → MongoDB | 10-50ms | Database write |
| MongoDB → Event Bus | <1ms | Same process |
| Event Bus → Plugins | 100-300ms | Data loading + processing |
| Plugins → WebSocket | <10ms | Broadcast |
| WebSocket → Client | 50-200ms | Network |
| Total | 5-6 minutes | CGM interval is dominant |
| Factor | Impact | Mitigation |
|---|---|---|
| Heartbeat interval | 0-60s delay | Reduce interval (trade-off: resources) |
| Debounce threshold | 5s delay | Reduce threshold |
| Plugin processing | 100-300ms | Optimize plugins |
| Network latency | Variable | CDN for static assets |
| Client processing | 50-100ms | Optimize JavaScript |
- Reduce heartbeat interval for critical updates (30s)
- Bypass debounce for urgent data
- Priority queue for alarm events
- Client prediction to compensate for delay
- Optimistic updates in UI
| Limitation | Impact | Threshold |
|---|---|---|
| Single process | No horizontal scaling | ~1000 concurrent connections |
| In-memory state | Lost on restart | N/A |
| No load balancing | Single point of failure | N/A |
| No connection limits | DoS vulnerability | N/A |
Vertical Scaling:
- Increase Node.js memory
- Use worker threads for CPU tasks
- Optimize event handlers
Horizontal Scaling:
┌─────────────┐
│ Load │
│ Balancer │
└──────┬──────┘
┌───────────────┼───────────────┐
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Nightscout │ │ Nightscout │ │ Nightscout │
│ Instance 1 │ │ Instance 2 │ │ Instance 3 │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
└───────────────┼───────────────┘
▼
┌─────────────┐
│ Redis │
│ Pub/Sub │
└─────────────┘
Requirements for Horizontal Scaling:
- Redis adapter for Socket.IO
- Shared session store
- Database connection pooling
- Sticky sessions (or Redis pub/sub)
Implementation:
const { createAdapter } = require('@socket.io/redis-adapter');
const { createClient } = require('redis');
const pubClient = createClient({ url: process.env.REDIS_URL });
const subClient = pubClient.duplicate();
io.adapter(createAdapter(pubClient, subClient));Current Behavior:
- Automatic reconnection (Socket.IO default)
- No connection health checks
- No graceful degradation
Recommendations:
- Implement connection heartbeat
- Add connection timeout handling
- Queue messages during disconnect
- Implement exponential backoff
Current Issues:
- Some errors silently swallowed
- No error event for clients
- No error aggregation
Recommendations:
socket.on('error', function(error) {
console.error('Socket error:', error);
// Notify monitoring
// Attempt recovery
});
io.engine.on('connection_error', function(err) {
console.error('Connection error:', err);
});Location: lib/bus.js
Current Implementation:
stream.teardown = function () {
console.log('Initiating server teardown');
clearInterval(busInterval);
stream.emit('teardown');
};Recommendations:
- Notify connected clients of shutdown
- Wait for pending operations
- Close connections gracefully
- Implement shutdown timeout
- Connection count (via Socket.IO)
- Basic console logging
| Metric | Type | Purpose |
|---|---|---|
socket_connections_total |
Gauge | Active connections |
socket_messages_sent_total |
Counter | Message volume |
socket_message_latency_ms |
Histogram | Performance |
event_bus_events_total |
Counter | Internal events |
data_update_latency_ms |
Histogram | Update pipeline |
| Condition | Threshold | Action |
|---|---|---|
| Connection drop | >50% in 5min | Alert |
| Message latency | >5s p99 | Alert |
| Event bus backlog | >100 events | Warn |
| Memory usage | >80% | Warn |
- Default namespace: Optional auth
- Storage/Alarm namespaces: Required auth
- Token validated per subscription
- Storage: Per-collection permission check
- Alarm: Any valid token accepted
Current State: No rate limiting on WebSocket
Recommendations:
// Limit events per client
const rateLimit = require('socket-rate-limiter');
io.use(rateLimit({
points: 100, // 100 events
duration: 60 // per minute
}));- Add connection rate limiting - Prevent DoS
- Implement proper error handling - Reliability
- Add health check endpoint - Monitoring
- Add Redis adapter for horizontal scaling
- Implement connection metrics - Observability
- Add message queue for reliability
- Reduce heartbeat interval - Lower latency
- Implement graceful shutdown - Zero downtime
- Add TypeScript definitions - Developer experience
- WebSocket compression - Bandwidth reduction
- Binary protocol option - Performance
- Event sourcing - Audit trail