Skip to content

Automatic Reconnection

Automatic reconnection is a critical feature for resilient real-time applications. This guide covers configuration and advanced patterns.

Table of Contents

How It Works

Reconnection Flow

  1. Disconnection: Connection to the server is lost
  2. Wait: Waits for ReconnectDelay before the first attempt
  3. Attempt: Attempts to reconnect
  4. Backoff: If it fails, the delay increases exponentially (capped by ReconnectDelayMax)
  5. Repeat: Repeats up to ReconnectAttempts times
  6. Failure: If all attempts fail, OnReconnectFailed is emitted

Exponential Backoff

The delay between attempts grows exponentially:

Attempt 1: ReconnectDelay (e.g., 1s)
Attempt 2: ReconnectDelay * 2 (e.g., 2s)
Attempt 3: ReconnectDelay * 4 (e.g., 4s)
Attempt 4: min(ReconnectDelay * 8, ReconnectDelayMax) (e.g., 5s if max=5s)
Attempt 5: ReconnectDelayMax (e.g., 5s)

Configuration

Basic Configuration

client := socketio.New("ws://localhost:3000", socketio.Options{
  ReconnectAttempts: 5,                   // Maximum 5 attempts
  ReconnectDelay:    time.Second,         // First attempt after 1s
  ReconnectDelayMax: 5 * time.Second,     // Maximum 5s between attempts
})

Aggressive Reconnection

For applications that need to reconnect quickly:

client := socketio.New("ws://localhost:3000", socketio.Options{
  ReconnectAttempts: 10,                  // Many attempts
  ReconnectDelay:    500 * time.Millisecond, // Try fast
  ReconnectDelayMax: 3 * time.Second,     // Don't wait long
})

Conservative Reconnection

For applications that don't want to overload the server:

client := socketio.New("ws://localhost:3000", socketio.Options{
  ReconnectAttempts: 3,                   // Few attempts
  ReconnectDelay:    2 * time.Second,     // Wait longer
  ReconnectDelayMax: 30 * time.Second,    // Allow long delays
})

No Automatic Reconnection

For complete manual control:

client := socketio.New("ws://localhost:3000", socketio.Options{
  ReconnectAttempts: 0, // Disable automatic reconnection
})

client.OnDisconnect(func(reason string) {
  // Implement custom logic
  go manualReconnect()
})

Infinite Reconnection

Use with caution - only for critical applications:

client := socketio.New("ws://localhost:3000", socketio.Options{
  ReconnectAttempts: -1, // Infinite attempts
  ReconnectDelayMax: 60 * time.Second, // Cap maximum delay
})

Reconnection Events

OnReconnectAttempt

Executes before each reconnection attempt:

var attemptTime time.Time

client.OnReconnectAttempt(func(attempt int) {
  attemptTime = time.Now()
  fmt.Printf("πŸ”„ Reconnection attempt #%d at %s\n",
    attempt, attemptTime.Format("15:04:05"))

  // Additional logic
  if attempt > 3 {
    fmt.Println("⚠️  Multiple failed attempts, check your connection")
  }
})

OnReconnect

Executes when reconnection is successful:

client.OnReconnect(func(attempt int) {
  duration := time.Since(attemptTime)
  fmt.Printf("βœ… Reconnected after %d attempts (%v)\n",
    attempt, duration)

  // Re-subscribe to events
  client.Emit("re-subscribe", previousSubscriptions)

  // Sync state
  syncLocalState()
})

OnReconnectError

Executes when an attempt fails:

client.OnReconnectError(func(err error) {
  fmt.Printf("❌ Reconnection error: %v\n", err)

  // Diagnostics
  if strings.Contains(err.Error(), "connection refused") {
    fmt.Println("Server may be down")
  } else if strings.Contains(err.Error(), "timeout") {
    fmt.Println("Network issue or slow server")
  }
})

OnReconnectFailed

Executes when all attempts fail:

client.OnReconnectFailed(func() {
  fmt.Println("πŸ’₯ Reconnection failed after all attempts")

  // Notify user
  showUserNotification("Connection Lost", "Could not reconnect to server")

  // Fallback
  switchToOfflineMode()

  // Or prompt for manual reconnection
  promptManualReconnect()
})

Implementation Patterns

Pattern: Reconnection State

Maintain and display reconnection state to the user:

type ReconnectionState struct {
  Attempting     bool
  Attempt        int
  MaxAttempts    int
  LastError      error
  StartTime      time.Time
  mu             sync.RWMutex
}

func (rs *ReconnectionState) OnAttempt(attempt int) {
  rs.mu.Lock()
  defer rs.mu.Unlock()

  rs.Attempting = true
  rs.Attempt = attempt

  if rs.StartTime.IsZero() {
    rs.StartTime = time.Now()
  }
}

func (rs *ReconnectionState) OnSuccess() {
  rs.mu.Lock()
  defer rs.mu.Unlock()

  duration := time.Since(rs.StartTime)
  fmt.Printf("βœ… Reconnected in %v\n", duration)

  rs.reset()
}

func (rs *ReconnectionState) OnError(err error) {
  rs.mu.Lock()
  defer rs.mu.Unlock()

  rs.LastError = err
}

func (rs *ReconnectionState) OnFailed() {
  rs.mu.Lock()
  defer rs.mu.Unlock()

  duration := time.Since(rs.StartTime)
  fmt.Printf("πŸ’₯ Failed after %v\n", duration)

  rs.reset()
}

func (rs *ReconnectionState) reset() {
  rs.Attempting = false
  rs.Attempt = 0
  rs.LastError = nil
  rs.StartTime = time.Time{}
}

func (rs *ReconnectionState) GetStatus() string {
  rs.mu.RLock()
  defer rs.mu.RUnlock()

  if !rs.Attempting {
    return "Connected"
  }

  return fmt.Sprintf("Reconnecting... (attempt %d/%d)",
    rs.Attempt, rs.MaxAttempts)
}

// Usage
state := &ReconnectionState{MaxAttempts: 5}

client.OnReconnectAttempt(func(attempt int) {
  state.OnAttempt(attempt)
  updateUI(state.GetStatus())
})

client.OnReconnect(func(attempt int) {
  state.OnSuccess()
  updateUI("Connected")
})

client.OnReconnectError(func(err error) {
  state.OnError(err)
})

client.OnReconnectFailed(func() {
  state.OnFailed()
  updateUI("Disconnected")
})

Pattern: State Re-synchronization

Synchronize application state after reconnecting:

type AppState struct {
  subscriptions []string
  rooms         []string
  lastMessageId string
  userData      map[string]interface{}
}

func (app *AppState) Save() {
  // Save state to localStorage, disk, etc.
}

func (app *AppState) Restore() {
  // Restore saved state
}

func setupReconnectionSync(client *socketio.Socket, state *AppState) {
  client.OnDisconnect(func(reason string) {
    // Save state before losing connection
    state.Save()
  })

  client.OnReconnect(func(attempt int) {
    // Restore state
    state.Restore()

    // Re-subscribe
    for _, subscription := range state.subscriptions {
      client.Emit("subscribe", subscription)
    }

    // Re-join rooms
    for _, room := range state.rooms {
      client.Emit("join-room", room)
    }

    // Sync missed messages
    if state.lastMessageId != "" {
      client.Emit("sync-messages", map[string]interface{}{
        "since": state.lastMessageId,
      })
    }
  })
}

Pattern: Offline Queue

Queue operations while disconnected:

type OfflineQueue struct {
  client *socketio.Socket
  queue  []QueuedEvent
  mu     sync.Mutex
}

type QueuedEvent struct {
  Event string
  Data  []interface{}
  Time  time.Time
}

func NewOfflineQueue(client *socketio.Socket) *OfflineQueue {
  oq := &OfflineQueue{
    client: client,
    queue:  make([]QueuedEvent, 0),
  }

  oq.setupHandlers()
  return oq
}

func (oq *OfflineQueue) setupHandlers() {
  oq.client.OnReconnect(func(attempt int) {
    oq.flush()
  })
}

func (oq *OfflineQueue) Emit(event string, data ...interface{}) {
  if oq.client.IsConnected() {
    oq.client.Emit(event, data...)
  } else {
    oq.enqueue(event, data...)
  }
}

func (oq *OfflineQueue) enqueue(event string, data ...interface{}) {
  oq.mu.Lock()
  defer oq.mu.Unlock()

  oq.queue = append(oq.queue, QueuedEvent{
    Event: event,
    Data:  data,
    Time:  time.Now(),
  })

  fmt.Printf("πŸ“¦ Event queued: %s (total: %d)\n", event, len(oq.queue))
}

func (oq *OfflineQueue) flush() {
  oq.mu.Lock()
  defer oq.mu.Unlock()

  if len(oq.queue) == 0 {
    return
  }

  fmt.Printf("πŸ“€ Sending %d queued events...\n", len(oq.queue))

  for _, qe := range oq.queue {
    // Check if event is not too old (e.g., max 5 minutes)
    if time.Since(qe.Time) < 5*time.Minute {
      oq.client.Emit(qe.Event, qe.Data...)
    } else {
      fmt.Printf("⏱️  Event too old, discarded: %s\n", qe.Event)
    }
  }

  oq.queue = make([]QueuedEvent, 0)
  fmt.Println("βœ… Queue flushed")
}

// Usage
queue := NewOfflineQueue(client)

// Use queue instead of client directly
queue.Emit("message", "Hello") // Will send immediately or queue

Pattern: Custom Backoff Reconnection

Implement custom backoff strategy:

type CustomBackoff struct {
  client         *socketio.Socket
  attempts       int
  maxAttempts    int
  baseDelay      time.Duration
  maxDelay       time.Duration
  multiplier     float64
  jitter         bool
  reconnecting   bool
  mu             sync.Mutex
}

func NewCustomBackoff(client *socketio.Socket) *CustomBackoff {
  cb := &CustomBackoff{
    client:      client,
    maxAttempts: 10,
    baseDelay:   time.Second,
    maxDelay:    30 * time.Second,
    multiplier:  1.5, // Exponential backoff with factor 1.5
    jitter:      true, // Add jitter to avoid thundering herd
  }

  cb.setupHandlers()
  return cb
}

func (cb *CustomBackoff) setupHandlers() {
  cb.client.OnDisconnect(func(reason string) {
    if reason != "io client disconnect" {
      go cb.startReconnection()
    }
  })
}

func (cb *CustomBackoff) startReconnection() {
  cb.mu.Lock()
  if cb.reconnecting {
    cb.mu.Unlock()
    return
  }
  cb.reconnecting = true
  cb.attempts = 0
  cb.mu.Unlock()

  defer func() {
    cb.mu.Lock()
    cb.reconnecting = false
    cb.mu.Unlock()
  }()

  for cb.attempts < cb.maxAttempts {
    cb.attempts++

    // Calculate delay
    delay := cb.calculateDelay()
    fmt.Printf("⏱️  Waiting %v before attempt %d/%d\n",
      delay, cb.attempts, cb.maxAttempts)

    time.Sleep(delay)

    // Attempt to reconnect
    fmt.Printf("πŸ”„ Attempting to reconnect... (#%d)\n", cb.attempts)

    if cb.client.IsConnected() {
      fmt.Println("βœ… Reconnected successfully")
      cb.attempts = 0
      return
    }
  }

  fmt.Println("πŸ’₯ Reconnection failed after all attempts")
}

func (cb *CustomBackoff) calculateDelay() time.Duration {
  // Exponential backoff: baseDelay * (multiplier ^ (attempts - 1))
  delay := float64(cb.baseDelay) * math.Pow(cb.multiplier, float64(cb.attempts-1))

  // Cap at maximum
  if delay > float64(cb.maxDelay) {
    delay = float64(cb.maxDelay)
  }

  // Add jitter (randomize Β±25%)
  if cb.jitter {
    jitterAmount := delay * 0.25
    delay += (rand.Float64()*2 - 1) * jitterAmount
  }

  return time.Duration(delay)
}

Advanced Strategies

Strategy: Network-Adaptive

Adjust strategy based on network quality:

type AdaptiveReconnection struct {
  client           *socketio.Socket
  networkQuality   string // "good", "poor", "offline"
  failureCount     int
  successCount     int
}

func (ar *AdaptiveReconnection) adjustStrategy() {
  if ar.failureCount > 3 {
    ar.networkQuality = "poor"
  } else if ar.successCount > 5 {
    ar.networkQuality = "good"
  }

  // Adjust configuration based on quality
  switch ar.networkQuality {
  case "good":
    // Good network: try fast
    ar.client.ReconnectDelay = time.Second
    ar.client.ReconnectDelayMax = 5 * time.Second
    ar.client.ReconnectAttempts = 5

  case "poor":
    // Poor network: be more conservative
    ar.client.ReconnectDelay = 3 * time.Second
    ar.client.ReconnectDelayMax = 30 * time.Second
    ar.client.ReconnectAttempts = 10

  case "offline":
    // Offline: wait long between attempts
    ar.client.ReconnectDelay = 10 * time.Second
    ar.client.ReconnectDelayMax = 60 * time.Second
    ar.client.ReconnectAttempts = -1 // Infinite
  }
}

Strategy: Health Checks

Proactively verify connection health:

type ConnectionHealthChecker struct {
  client      *socketio.Socket
  interval    time.Duration
  timeout     time.Duration
  stop        chan struct{}
  unhealthy   bool
}

func NewHealthChecker(client *socketio.Socket) *ConnectionHealthChecker {
  hc := &ConnectionHealthChecker{
    client:   client,
    interval: 30 * time.Second,
    timeout:  5 * time.Second,
    stop:     make(chan struct{}),
  }

  go hc.start()
  return hc
}

func (hc *ConnectionHealthChecker) start() {
  ticker := time.NewTicker(hc.interval)
  defer ticker.Stop()

  for {
    select {
    case <-hc.stop:
      return
    case <-ticker.C:
      hc.check()
    }
  }
}

func (hc *ConnectionHealthChecker) check() {
  if !hc.client.IsConnected() {
    return
  }

  done := make(chan bool, 1)

  // Send ping
  hc.client.EmitWithAck("ping", func(response ...interface{}) {
    done <- response != nil
  }, time.Now().Unix())

  // Wait for response with timeout
  select {
  case healthy := <-done:
    if healthy {
      hc.unhealthy = false
      fmt.Println("πŸ’š Connection healthy")
    } else {
      hc.handleUnhealthy()
    }
  case <-time.After(hc.timeout):
    hc.handleUnhealthy()
  }
}

func (hc *ConnectionHealthChecker) handleUnhealthy() {
  if !hc.unhealthy {
    hc.unhealthy = true
    fmt.Println("⚠️  Connection unhealthy, forcing reconnection...")

    // Force reconnection
    hc.client.Close()
  }
}

func (hc *ConnectionHealthChecker) Stop() {
  close(hc.stop)
}

Strategy: Fallback to HTTP Long-Polling

Switch to more reliable transport if WebSocket fails:

type FallbackTransport struct {
  client         *socketio.Socket
  wsURL          string
  pollingURL     string
  failureCount   int
  usingPolling   bool
}

func (ft *FallbackTransport) setupHandlers() {
  ft.client.OnReconnectError(func(err error) {
    ft.failureCount++

    // After 3 failures, try polling
    if ft.failureCount >= 3 && !ft.usingPolling {
      fmt.Println("πŸ”„ Switching to HTTP Long-Polling...")
      ft.switchToPolling()
    }
  })

  ft.client.OnReconnect(func(attempt int) {
    ft.failureCount = 0

    // Try switching back to WebSocket after successful reconnection
    if ft.usingPolling {
      go ft.tryWebSocketAgain()
    }
  })
}

func (ft *FallbackTransport) switchToPolling() {
  ft.client.Close()
  // Create new client with polling (requires library modification)
  // ft.client = socketio.NewWithTransport(ft.pollingURL, "polling")
  ft.usingPolling = true
}

func (ft *FallbackTransport) tryWebSocketAgain() {
  time.Sleep(5 * time.Minute) // Wait before retrying

  if ft.usingPolling {
    fmt.Println("πŸ”„ Trying to switch back to WebSocket...")
    // Attempt to create WebSocket client
    // If it fails, keep polling
  }
}

Best Practices

1. Configure Appropriate Timeouts

// βœ… Good: configure all related timeouts
client := socketio.New("ws://localhost:3000", socketio.Options{
  Timeout:           30 * time.Second, // Initial connection
  AckTimeout:        5 * time.Second,  // Acknowledgments
  ReconnectDelay:    time.Second,
  ReconnectDelayMax: 5 * time.Second,
  ReconnectAttempts: 5,
})

2. Handle All Events

// βœ… Good: handle all reconnection events
client.OnReconnectAttempt(func(attempt int) {
  log.Printf("Reconnecting... (#%d)", attempt)
})

client.OnReconnect(func(attempt int) {
  log.Printf("Reconnected (#%d)", attempt)
  resyncState()
})

client.OnReconnectError(func(err error) {
  log.Printf("Error: %v", err)
})

client.OnReconnectFailed(func() {
  log.Println("Reconnection failed")
  notifyUser()
})

3. Re-subscribe After Reconnecting

// βœ… Good: always re-subscribe
var subscriptions []string

client.OnConnect(func() {
  for _, sub := range subscriptions {
    client.Emit("subscribe", sub)
  }
})

client.OnReconnect(func(attempt int) {
  // Re-subscribe automatically
  for _, sub := range subscriptions {
    client.Emit("subscribe", sub)
  }
})

4. Don't Trust Connection Is Always Available

// βœ… Good: check connection before critical operations
if !client.IsConnected() {
  return fmt.Errorf("no connection")
}

client.Emit("important-event", data)

// ❌ Bad: assume always connected
client.Emit("important-event", data) // May fail silently

5. Notify the User

// βœ… Good: inform user about state
client.OnReconnectAttempt(func(attempt int) {
  showNotification("Reconnecting...", fmt.Sprintf("Attempt %d", attempt))
})

client.OnReconnect(func(attempt int) {
  showNotification("Connected", "Connection restored")
})

client.OnReconnectFailed(func() {
  showError("No Connection", "Could not connect to server")
})

See Also