Designing Online/Offline Indicator
| 8 min read
tl;dr: Building reliable connection state indicators: heartbeat mechanisms, state machines, browser APIs, and handling network transitions gracefully in production systems.
Building a reliable online/offline indicator seems straightforward—until you deploy it to production. Users on flaky networks, mobile devices switching between WiFi and cellular, and servers behind load balancers create edge cases that break naive implementations.
Here’s how to build one that works in the real world.
The Problem Space
Connection state detection isn’t binary. Real networks exist in states between “fully connected” and “completely offline.” Users experience:
- Intermittent connectivity: WiFi that drops packets but maintains a connection
- Slow networks: Connections that timeout before responses arrive
- Partial failures: DNS works but HTTP doesn’t, or vice versa
- Server-side issues: Your server is up, but the load balancer is down
A good indicator must handle all of these gracefully.
System Architecture
graph TB
Client[Client Application] -->|HTTP/WebSocket| Network[Network Layer]
Network -->|Connection| Server[Server]
Client -->|Heartbeat| Server
Server -->|ACK| Client
Client --> BrowserAPI[Browser APIs<br/>navigator.onLine<br/>Network Info API]
Server --> Monitor[Connection Monitor<br/>Session Store]
Browser APIs: The Foundation
The browser provides navigator.onLine, but it’s unreliable. It only checks if the device has a network interface, not whether it can reach your servers.
// Don't rely solely on this
if (navigator.onLine) {
// Might still be offline from your server's perspective
}
The Network Information API provides more detail:
const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
if (connection) {
console.log(connection.effectiveType); // '4g', '3g', 'slow-2g'
console.log(connection.downlink); // Mbps
console.log(connection.rtt); // Round-trip time
}
Use these APIs for fast initial detection, but always verify with your backend.
Heartbeat Mechanism
The most reliable approach is periodic heartbeats—small requests that confirm both network connectivity and server availability.
Heartbeat Flow
sequenceDiagram
participant C as Client
participant S as Server
Note over C,S: Normal Operation
C->>S: Heartbeat (T0)
S->>C: ACK (T1)
Note over C,S: Timeout Scenario
C->>S: Heartbeat (T2)
Note over S: No Response
Note over C: Timeout (T3)
Note over C: State: Online → Offline
Implementation Strategy
1. Configurable Intervals
Start with a reasonable default (30 seconds), but make it configurable based on network quality:
class ConnectionMonitor {
constructor() {
this.interval = 30000; // 30s default
this.timeout = 5000; // 5s timeout
this.failureThreshold = 3; // Mark offline after 3 failures
this.failures = 0;
}
async sendHeartbeat() {
try {
const response = await fetch('/api/heartbeat', {
method: 'HEAD',
signal: AbortSignal.timeout(this.timeout)
});
if (response.ok) {
this.failures = 0;
return true;
}
} catch (error) {
this.failures++;
return false;
}
}
}
2. Exponential Backoff
When offline, don’t hammer the server. Use exponential backoff for reconnection attempts:
class ReconnectionStrategy {
constructor() {
this.baseDelay = 1000; // 1 second
this.maxDelay = 60000; // 1 minute
this.currentDelay = this.baseDelay;
}
async reconnect() {
while (!this.isConnected) {
await this.attemptConnection();
await this.delay(this.currentDelay);
this.currentDelay = Math.min(
this.currentDelay * 2,
this.maxDelay
);
}
this.currentDelay = this.baseDelay; // Reset on success
}
}
3. Bidirectional Confirmation
The server should also track client connections. If a client hasn’t sent a heartbeat in 2-3 intervals, mark it as disconnected server-side.
State Machine Design
Connection state isn’t binary. Use a state machine to model transitions:
State Machine
stateDiagram-v2
[*] --> Online
Online --> Offline: Connection Lost<br/>Timeout/Error
Online --> Degraded: Intermittent Failures
Offline --> Connecting: Reconnect Attempt
Connecting --> Online: Connection Established
Connecting --> Offline: Reconnect Failed
Degraded --> Online: Stability Restored
Degraded --> Offline: Complete Failure
Online --> Online: Heartbeat OK
Offline --> Offline: Still Disconnected
States:
- Online: Fully connected, receiving heartbeats
- Offline: No connection, queuing operations
- Connecting: Actively attempting to reconnect
- Degraded: Intermittent failures, reduced functionality
Transitions:
class ConnectionStateMachine {
constructor() {
this.state = 'online';
this.listeners = [];
}
transition(newState, reason) {
const validTransitions = {
online: ['offline', 'degraded'],
offline: ['connecting'],
connecting: ['online', 'offline'],
degraded: ['online', 'offline']
};
if (validTransitions[this.state]?.includes(newState)) {
const oldState = this.state;
this.state = newState;
this.notifyListeners(oldState, newState, reason);
}
}
notifyListeners(oldState, newState, reason) {
this.listeners.forEach(listener => {
listener({ oldState, newState, reason });
});
}
}
Network Detection Strategies
Different strategies have different trade-offs:
Detection Strategies Comparison
Browser API Only
Pros: Fast, no server overhead
Cons: Unreliable, false positives
Use case: Initial optimistic check
Heartbeat Only
Pros: Accurate, server status confirmed
Cons: Latency delay, server overhead
Use case: Critical applications
Hybrid Approach (Recommended)
Pros: Fast initial detection + accurate verification
Cons: More complex state management
Use case: Production applications
Recommended Implementation Flow
flowchart LR
A[Check navigator.onLine] -->|Fast Check| B{Online?}
B -->|No| C[Immediate Offline State]
B -->|Yes| D[Send Heartbeat]
D --> E{Response?}
E -->|Success| F[Online State]
E -->|Timeout| G[Mark Offline]
E -->|Intermittent| H[Degraded State]
Recommended Flow
- Check
navigator.onLine- Fast initial check - Send heartbeat - Verify server connectivity
- Wait for response - With timeout
- Update state - Based on result, notify UI
class HybridConnectionMonitor {
async checkConnection() {
// Fast check first
if (!navigator.onLine) {
this.transition('offline', 'browser-api');
return;
}
// Verify with heartbeat
const isConnected = await this.sendHeartbeat();
if (isConnected) {
this.transition('online', 'heartbeat-success');
} else {
this.handleFailure();
}
}
handleFailure() {
this.failures++;
if (this.failures >= this.failureThreshold) {
this.transition('offline', 'heartbeat-timeout');
} else if (this.failures >= 2) {
this.transition('degraded', 'intermittent-failures');
}
}
}
Handling Edge Cases
Flaky Connections
Networks that work sometimes need special handling:
class FlakyConnectionHandler {
constructor() {
this.recentFailures = [];
this.windowSize = 10; // Track last 10 attempts
}
recordAttempt(success) {
this.recentFailures.push({
success,
timestamp: Date.now()
});
// Keep only recent attempts
if (this.recentFailures.length > this.windowSize) {
this.recentFailures.shift();
}
const failureRate = this.calculateFailureRate();
if (failureRate > 0.5) {
return 'degraded';
} else if (failureRate === 1.0) {
return 'offline';
}
return 'online';
}
}
Timeout Configuration
Different operations need different timeouts:
- Heartbeat: 3-5 seconds (fast failure)
- API calls: 10-30 seconds (user-initiated)
- File uploads: 60+ seconds (long operations)
const timeouts = {
heartbeat: 5000,
api: 10000,
upload: 60000
};
// Use appropriate timeout per operation
const response = await fetch(url, {
signal: AbortSignal.timeout(timeouts.api)
});
Server-Side Considerations
Your server needs to handle connection state too:
- Session tracking: Track last heartbeat per client
- Cleanup: Remove stale sessions after timeout
- Load balancing: Ensure heartbeats hit the same server (sticky sessions) or use shared state
# Example server-side tracking
class ConnectionTracker:
def __init__(self):
self.connections = {} # client_id -> last_heartbeat
def record_heartbeat(self, client_id):
self.connections[client_id] = time.time()
def get_online_clients(self, timeout=90):
now = time.time()
return [
client_id for client_id, last_seen in self.connections.items()
if now - last_seen < timeout
]
UI/UX Considerations
How you present connection state matters:
Visual Indicators:
- Green dot: Online
- Yellow dot: Degraded/Connecting
- Red dot: Offline
- Pulsing animation: Actively checking
User Actions:
- Online: Full functionality
- Degraded: Show warning, allow retry
- Offline: Queue operations, show “will sync when online”
Notifications:
- Don’t spam users with every state change
- Show notifications only on significant transitions (online → offline, offline → online)
- Use toast notifications, not blocking modals
class ConnectionUI {
updateIndicator(state) {
const indicator = document.getElementById('connection-indicator');
const states = {
online: { color: 'green', text: 'Online' },
offline: { color: 'red', text: 'Offline' },
degraded: { color: 'yellow', text: 'Unstable' },
connecting: { color: 'blue', text: 'Connecting...' }
};
const config = states[state];
indicator.className = `connection-${config.color}`;
indicator.textContent = config.text;
}
showNotification(oldState, newState) {
// Only notify on significant transitions
const significant = [
['online', 'offline'],
['offline', 'online']
];
if (significant.some(([old, new]) => old === oldState && new === newState)) {
this.toast(`${oldState} → ${newState}`);
}
}
}
Performance Considerations
Connection monitoring has costs:
Client-side:
- Battery drain (mobile devices)
- Network usage (heartbeat requests)
- CPU usage (state management)
Server-side:
- Request overhead (heartbeat endpoints)
- Memory (connection tracking)
- Database writes (if persisting state)
Optimizations:
- Adaptive intervals: Increase heartbeat interval when stable, decrease when unstable
- Batch operations: Queue operations when offline, send in batch when online
- Smart polling: Only poll when tab is active (Page Visibility API)
// Adaptive heartbeat interval
class AdaptiveHeartbeat {
constructor() {
this.baseInterval = 30000;
this.currentInterval = this.baseInterval;
this.stableCount = 0;
}
onHeartbeatSuccess() {
this.stableCount++;
// Increase interval if stable for 5+ cycles
if (this.stableCount >= 5) {
this.currentInterval = Math.min(
this.currentInterval * 1.5,
300000 // Max 5 minutes
);
}
}
onHeartbeatFailure() {
this.stableCount = 0;
this.currentInterval = this.baseInterval; // Reset to base
}
}
Testing Strategies
Test your implementation under various conditions:
- Network throttling: Chrome DevTools → Network → Throttling
- Offline simulation:
navigator.onLine = false - Server failures: Kill your server, restart it
- Timeout scenarios: Set very short timeouts
- Rapid state changes: Toggle network on/off quickly
// Test helper
class ConnectionTester {
async testOfflineScenario() {
// Simulate offline
navigator.onLine = false;
// Wait for state transition
await this.waitForState('offline', 5000);
// Verify UI updates
assert.equal(this.getIndicatorState(), 'offline');
// Simulate online
navigator.onLine = true;
await this.monitor.sendHeartbeat();
// Verify recovery
await this.waitForState('online', 10000);
}
}
Key Takeaways
- Never trust
navigator.onLinealone - Always verify with your backend - Use state machines - Connection state has more than two values
- Implement exponential backoff - Don’t hammer servers when reconnecting
- Handle flaky connections - Degraded state is as important as offline
- Optimize for battery - Adaptive intervals, Page Visibility API
- Test edge cases - Network conditions vary wildly in production
Building a reliable online/offline indicator requires thinking beyond simple boolean checks. It’s a distributed systems problem that needs careful state management, timeout handling, and graceful degradation.
The best implementations combine browser APIs for fast detection with heartbeat mechanisms for accuracy, wrapped in a state machine that handles the complexity of real-world networks.
Building reliable connection monitoring? I provide architecture reviews, system design consultation, and engineering guidance. Let's discuss your implementation.
P.S. Follow me on Twitter where I share engineering insights, system design patterns, and technical leadership perspectives.
Enjoyed this? Support my work
Buy me a coffee