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¶
- Disconnection: Connection to the server is lost
- Wait: Waits for
ReconnectDelaybefore the first attempt - Attempt: Attempts to reconnect
- Backoff: If it fails, the delay increases exponentially (capped by
ReconnectDelayMax) - Repeat: Repeats up to
ReconnectAttemptstimes - Failure: If all attempts fail,
OnReconnectFailedis 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")
})