Skip to main content

WebSocket Connection Management

SDD Classification: L3-Technical Authority: Engineering Team Review Cycle: Quarterly
This guide covers WebSocket connection establishment, lifecycle management, reconnection strategies, and best practices for maintaining reliable real-time connections.

Establishing a Connection

Connection URL Format

wss://api.materi.dev/v1/ws/documents/{document_id}?token={jwt_token}&client_id={unique_client_id}

Basic Connection Example

class MateriWebSocket {
  constructor(documentId, accessToken) {
    this.documentId = documentId;
    this.accessToken = accessToken;
    this.clientId = this.generateClientId();
    this.ws = null;
    this.isConnected = false;
    this.reconnectAttempts = 0;
    this.maxReconnectAttempts = 5;
    this.messageQueue = [];
  }

  connect() {
    const wsUrl = `wss://api.materi.dev/v1/ws/documents/${this.documentId}?token=${this.accessToken}&client_id=${this.clientId}`;

    this.ws = new WebSocket(wsUrl);

    this.ws.onopen = this.handleOpen.bind(this);
    this.ws.onmessage = this.handleMessage.bind(this);
    this.ws.onclose = this.handleClose.bind(this);
    this.ws.onerror = this.handleError.bind(this);
  }

  handleOpen(event) {
    console.log('WebSocket connected');
    this.isConnected = true;
    this.reconnectAttempts = 0;

    // Send any queued messages
    this.flushMessageQueue();

    // Start heartbeat
    this.startHeartbeat();
  }

  handleMessage(event) {
    const message = JSON.parse(event.data);
    this.processMessage(message);
  }

  handleClose(event) {
    console.log('WebSocket disconnected:', event.code, event.reason);
    this.isConnected = false;
    this.stopHeartbeat();

    // Attempt reconnection if not intentional
    if (event.code !== 1000 && this.reconnectAttempts < this.maxReconnectAttempts) {
      this.reconnect();
    }
  }

  handleError(error) {
    console.error('WebSocket error:', error);
  }

  generateClientId() {
    return 'client_' + Math.random().toString(36).substr(2, 9);
  }
}

Connection Lifecycle

Connection Acknowledgment

Upon successful connection, the server sends a connection acknowledgment:
{
  "type": "system",
  "event": "connection_ack",
  "data": {
    "client_id": "client_abc123",
    "user_id": "user_123",
    "permissions": ["read", "write", "comment"],
    "document_version": 25,
    "server_time": "2025-01-07T10:30:00Z"
  }
}

Heartbeat Management

Maintain connection health with periodic heartbeats:
class HeartbeatManager {
  constructor(ws, interval = 30000) {
    this.ws = ws;
    this.interval = interval;
    this.timer = null;
    this.lastPong = Date.now();
    this.timeout = interval * 2;
  }

  start() {
    this.timer = setInterval(() => {
      if (Date.now() - this.lastPong > this.timeout) {
        console.warn('Heartbeat timeout, reconnecting...');
        this.ws.close(4000, 'Heartbeat timeout');
        return;
      }

      this.ws.send(JSON.stringify({
        type: 'system',
        event: 'ping',
        data: { timestamp: Date.now() }
      }));
    }, this.interval);
  }

  handlePong(timestamp) {
    this.lastPong = Date.now();
    const latency = this.lastPong - timestamp;
    console.log(`Connection latency: ${latency}ms`);
  }

  stop() {
    if (this.timer) {
      clearInterval(this.timer);
      this.timer = null;
    }
  }
}

Heartbeat Configuration

ParameterDefaultDescription
Ping interval30 secondsHow often to send pings
Pong timeout60 secondsMax time to wait for pong
Server ping30 secondsServer-initiated pings

Reconnection Strategy

Exponential Backoff

class ReconnectionManager {
  constructor(wsClient) {
    this.wsClient = wsClient;
    this.baseDelay = 1000;
    this.maxDelay = 30000;
    this.maxAttempts = 10;
    this.attempts = 0;
  }

  scheduleReconnect() {
    if (this.attempts >= this.maxAttempts) {
      console.error('Max reconnection attempts reached');
      this.wsClient.onPermanentFailure();
      return;
    }

    // Exponential backoff with jitter
    const delay = Math.min(
      this.baseDelay * Math.pow(2, this.attempts) + Math.random() * 1000,
      this.maxDelay
    );

    this.attempts++;
    console.log(`Reconnecting in ${Math.round(delay)}ms (attempt ${this.attempts}/${this.maxAttempts})`);

    setTimeout(() => {
      this.wsClient.connect();
    }, delay);
  }

  reset() {
    this.attempts = 0;
  }

  getNextDelay() {
    return Math.min(
      this.baseDelay * Math.pow(2, this.attempts),
      this.maxDelay
    );
  }
}

Reconnection Delays

AttemptDelayTotal Time
11s1s
22s3s
34s7s
48s15s
516s31s
6+30s (max)varies

Message Queuing

Queue messages during disconnection for delivery upon reconnection:
class MessageQueue {
  constructor(maxSize = 1000) {
    this.queue = [];
    this.maxSize = maxSize;
  }

  enqueue(message) {
    if (this.queue.length >= this.maxSize) {
      // Remove oldest messages if queue is full
      this.queue.shift();
    }
    this.queue.push({
      message,
      timestamp: Date.now(),
      id: this.generateId()
    });
  }

  flush(sendFn) {
    const messages = this.queue.splice(0);
    messages.forEach(item => {
      sendFn(item.message);
    });
    return messages.length;
  }

  clear() {
    this.queue = [];
  }

  get length() {
    return this.queue.length;
  }

  generateId() {
    return 'msg_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
  }
}

Connection Recovery

State Synchronization

After reconnection, synchronize state with the server:
async function syncAfterReconnect(ws, lastKnownVersion) {
  // Request missed operations
  ws.send(JSON.stringify({
    type: 'system',
    event: 'sync_request',
    data: {
      last_version: lastKnownVersion,
      client_state: 'reconnecting'
    }
  }));

  // Server responds with sync data
  // {
  //   "type": "system",
  //   "event": "document_sync",
  //   "data": {
  //     "document": { ... },
  //     "missed_operations": [ ... ],
  //     "current_version": 30
  //   }
  // }
}

Recovery Response

{
  "type": "system",
  "event": "document_sync",
  "data": {
    "document": {
      "id": "doc_abc123",
      "version": 30,
      "content": "Complete document content..."
    },
    "missed_operations": [
      {
        "type": "insert",
        "position": 100,
        "content": "missed text",
        "version": 26
      }
    ],
    "active_users": [
      {
        "user_id": "user_456",
        "cursor_position": 150
      }
    ]
  }
}

Connection Limits

Per-User Limits

Limit TypeValueNotes
Per document5Same user, same document
Total connections20Across all documents
Connection rate10/minNew connections per minute

Handling Limit Exceeded

{
  "type": "system",
  "event": "error",
  "data": {
    "error_code": "CONNECTION_LIMIT_EXCEEDED",
    "message": "Maximum connections reached for this document",
    "details": {
      "current_connections": 5,
      "max_connections": 5
    }
  }
}

Graceful Disconnection

Client-Initiated Close

function disconnect() {
  // Save any pending changes
  flushMessageQueue();

  // Send disconnect notification
  ws.send(JSON.stringify({
    type: 'system',
    event: 'disconnect',
    data: {
      reason: 'user_initiated'
    }
  }));

  // Close with normal closure code
  ws.close(1000, 'Normal closure');
}

Close Codes

CodeNameDescription
1000NormalClean closure
1001Going AwayPage navigation or browser close
1006AbnormalConnection lost without close frame
4000Heartbeat TimeoutNo pong received
4001Auth ExpiredToken expired during session
4002Permission DeniedLost document access
4003Document LockedDocument locked for editing

Best Practices

Do

  1. Generate unique client IDs - Prevents duplicate connection issues
  2. Implement heartbeats - Detect dead connections early
  3. Queue messages during disconnect - Prevent data loss
  4. Use exponential backoff - Avoid overwhelming server
  5. Handle all close codes - Respond appropriately to each scenario

Don’t

  1. Don’t reconnect immediately - Always use backoff
  2. Don’t ignore close codes - They contain important information
  3. Don’t skip heartbeats - Connection may silently die
  4. Don’t queue indefinitely - Set reasonable limits
  5. Don’t assume connection state - Always verify


Document Status: Complete Version: 2.0